svghmi/gen_index_xhtml.ysl2
author Edouard Tisserant
Tue, 17 Mar 2020 10:34:26 +0100
branchsvghmi
changeset 2876 d2adbc273125
parent 2875 6a12e1084deb
child 2877 682bce953795
permissions -rw-r--r--
SVGHMI: moved debug code
include yslt_noindent.yml2

// overrides yslt's output function to set CDATA
decl output(method, cdata-section-elements="xhtml:script");

in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
    with "hmi_element", "$hmi_element";
    with "labels"{text *ptr};
};

in xsl decl optional_labels(*ptr, name="defs_by_labels") alias call-template {
    with "hmi_element", "$hmi_element";
    with "labels"{text *ptr};
    with "mandatory","'no'";
};

in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;
in xsl decl svgfunc(name, xmlns="http://www.w3.org/2000/svg") alias template;

!debug_output_calls = []

istylesheet
            /* From Inkscape */
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:cc="http://creativecommons.org/ns#"
            xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            xmlns:svg="http://www.w3.org/2000/svg"
            xmlns:xlink="http://www.w3.org/1999/xlink"
            xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
            xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
            xmlns:xhtml="http://www.w3.org/1999/xhtml"

            /* Our namespace to invoke python code */
            xmlns:ns="beremiz"
            extension-element-prefixes="ns func exsl regexp str dyn"
            exclude-result-prefixes="ns str regexp exsl func dyn" {


    const "svg_root_id", "/svg:svg/@id";
    const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]";

    const "hmi_pages", "$hmi_elements[func:parselabel(@inkscape:label)/widget/@type = 'Page']";

    const "default_page" choose {
        when "count($hmi_pages) > 1" {
            const "Home_page", 
                "$hmi_pages[func:parselabel(@inkscape:label)/widget/arg[1]/@value = 'Home']";
            choose {
                when "$Home_page" > Home
                otherwise {
                    error "No Home page defined!";
                }
            }
        }
        when "count($hmi_pages) = 0" {
            error "No page defined!";
        }
        otherwise > «func:parselabel($hmi_pages/@inkscape:label)/widget/arg[1]/@value»
    }

    const "_categories" {
        noindex > HMI_ROOT
        noindex > HMI_PLC_STATUS
        noindex > HMI_CURRENT_PAGE
    }
    const "categories", "exsl:node-set($_categories)";

    include geometry.ysl2

    include detachable_elements.ysl2

    include hmi_tree.ysl2


    def "func:is_descendant_path" {
        param "descend";
        param "ancest";
        result "string-length($ancest) > 0 and starts-with($descend,$ancest)";
    }

    //////////////// Inline SVG

    // Identity template :
    //  - copy every attributes 
    //  - copy every sub-elements
    template "@* | node()", mode="inline_svg" {
      // use real xsl:copy instead copy-of alias from yslt.yml2
      if "not(@id = $discardable_elements/@id)"
          xsl:copy apply "@* | node()", mode="inline_svg";
    }

    // replaces inkscape's height and width hints. forces fit
    template "svg:svg/@width", mode="inline_svg";
    template "svg:svg/@height", mode="inline_svg";
    svgtmpl "svg:svg", mode="inline_svg" svg {
        attrib "preserveAspectRatio" > none
        attrib "height" > 100vh
        attrib "width" > 100vw
        apply "@* | node()", mode="inline_svg";
    }
    // ensure that coordinate in CSV file generated by inkscape are in default reference frame
    template "svg:svg[@viewBox!=concat('0 0 ', @width, ' ', @height)]", mode="inline_svg" {
        error > ViewBox settings other than X=0, Y=0 and Scale=1 are not supported
    }
    // ensure that coordinate in CSV file generated by inkscape match svg default unit
    template "sodipodi:namedview[@units!='px' or @inkscape:document-units!='px']", mode="inline_svg" {
        error > All units must be set to "px" in Inkscape's document properties
    }


    //////////////// Clone Unlinking

    // svg:use (inkscape's clones) inside a widgets are
    // replaced by real elements they refer in order to :
    //  - allow finding "needle" element in "meter" widget,
    //    even if "needle" is in a group refered by a svg use.
    //  - if "needle" is visible through a svg:use for
    //    each instance of the widget, then needle would show
    //    the same position in all instances
    //
    // For now, clone unlinkink applies to descendants of all widget except HMI:Page
    // TODO: narrow application of clone unlinking to active elements,
    //       while keeping static decoration cloned
    const "to_unlink", "$hmi_elements[not(@id = $hmi_pages)]//svg:use";
    svgtmpl "svg:use", mode="inline_svg"
    {
        choose {
            when "@id = $to_unlink/@id"
                call "unlink_clone";
            otherwise
                xsl:copy apply "@* | node()", mode="inline_svg";
        }
    }

    // to unlink a clone, an group containing a copy of target element is created
    // that way, style and transforms can be preserved
    const "_excluded_use_attrs" {
        name > href
        name > width
        name > height
        name > x
        name > y
    }
    const "excluded_use_attrs","exsl:node-set($_excluded_use_attrs)";

    svgfunc "unlink_clone"{
        g{
            // include non excluded attributes
            foreach "@*[not(local-name() = $excluded_use_attrs/name)]"
                attrib "{name()}" > «.»

            const "targetid","substring-after(@xlink:href,'#')";
            apply "//svg:*[@id = $targetid]", mode="unlink_clone"{
                with "seed","@id";
            }
        }
    }

    // clone unlinking is really similar to deep-copy
    // all nodes are sytematically copied
    svgtmpl "@id", mode="unlink_clone" {
        param "seed";
        attrib "id" > «$seed»_«.»
    }

    svgtmpl "@*", mode="unlink_clone" xsl:copy;

    // copying widgets would have unwanted effect
    // instead widget is refered through a svg:use.
    svgtmpl "svg:*", mode="unlink_clone" {
        param "seed";
        choose {
            // node recursive copy ends when finding a widget
            when "@id = $hmi_elements/@id" {
                // place a clone instead of copying
                use{
                    attrib "xlink:href" > «concat('#',@id)»
                }
            }
            otherwise {
                xsl:copy apply "@* | node()", mode="unlink_clone" {
                    with "seed","$seed";
                }
            }
        }
    }

    /*const "mark" > =HMI=\n*/

    const "result_svg" apply "/", mode="inline_svg";
    const "result_svg_ns", "exsl:node-set($result_svg)";

    template "/" {
        comment > Made with SVGHMI. https://beremiz.org

        // use python to call all debug output from included definitions
        // '&bug' is a workaround for pyPEG that choke on yml2 python results not parsing to a single call
        !"&bug {"+"\n".join(["comment {\n| \n| %s:\n call \"%s\";\n| \n}"%(n,n) for n in debug_output_calls]) +"}"!

        comment {
            | Unlinked :
            foreach "$to_unlink"{
                | «@id»
            }
        }
        /**/
        html xmlns="http://www.w3.org/1999/xhtml"
             xmlns:svg="http://www.w3.org/2000/svg"
             xmlns:xlink="http://www.w3.org/1999/xlink" {
            head;
            body style="margin:0;overflow:hidden;" {
                copy "$result_svg";
                script{
                    call "scripts";
                }
            }
        }
    }

    /*
        Parses:
        "HMI:WidgetType:param1:param2@path1@path2"

        Into:
        widget type="WidgetType" {
            arg value="param1";
            arg value="param2";
            path value="path1";
            path value="path2";
        }
    */

    def "func:parselabel" {
        param "label";
        const "description", "substring-after($label,'HMI:')";

        const "_args", "substring-before($description,'@')";
        const "args" choose {
            when "$_args" value "$_args";
            otherwise value "$description";
        }

        const "_type", "substring-before($args,':')";
        const "type" choose {
            when "$_type" value "$_type";
            otherwise value "$args";
        }

        const "ast" if "$type" widget {
            attrib "type" > «$type»
            foreach "str:split(substring-after($args, ':'), ':')" {
                arg {
                    attrib "value" > «.»
                }
            }
            const "paths", "substring-after($description,'@')";
            foreach "str:split($paths, '@')" {
                if "string-length(.) > 0" path {
                    attrib "value" > «.»
                    const "path", ".";
                    const "item", "$indexed_hmitree/*[@hmipath = $path]";
                    if "count($item) = 1"
                        attrib "index" > «$item/@index»
                }
            }
        }

        result "exsl:node-set($ast)";
    }

    function "scripts"
    {
        | //(function(){
        |
        | id = idstr => document.getElementById(idstr);
        |
        | var hmi_hash = [«$hmitree/@hash»]; 

        /* TODO re-enable
        ||
        function evaluate_js_from_descriptions() {
            var Page;
            var Input;
            var Display;
            var res = [];
        ||
        const "midmark" > \n«$mark»
        apply """//*[contains(child::svg:desc, $midmark) or \
                     starts-with(child::svg:desc, $mark)]""",2 
              mode="code_from_descs";
        ||
            return res;
        }
        ||
        */

        | var hmi_widgets = {
        foreach "$hmi_elements" {
            const "widget", "func:parselabel(@inkscape:label)/widget";
            const "eltid","@id";
        |   "«@id»": {
        |     type: "«$widget/@type»",
        |     args: [
            foreach "$widget/arg"
        |         "«@value»"`if "position()!=last()" > ,`
        |     ],
        |     indexes: [
            foreach "$widget/path" {
                choose {
                    when "not(@index)" {
                        warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
                    }
                    otherwise {
        |             «@index»`if "position()!=last()" > ,`
                    }
                }
            }
        |     ],
        |     element: id("«@id»"),
            apply "$widget", mode="widget_defs" with "hmi_element",".";
        |   }`if "position()!=last()" > ,`
        }
        | }
        |
        | var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
        |
        | var hmitree_types = [

        foreach "$indexed_hmitree/*" {
        |     /* «@index»  «@hmipath» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
        }

        | ]
        |

        | var detachable_elements = {
        foreach "$detachable_elements"{
        |     "«@id»":[id("«@id»"), id("«../@id»")]`if "position()!=last()" > ,`
        }
        | }

        |
        | var page_desc = {

        foreach "$hmi_pages" {
            const "desc", "func:parselabel(@inkscape:label)/widget";
            const "page", ".";
            const "p", "$geometry[@Id = $page/@id]";

            const "page_all_elements", "func:all_related_elements($page)";

            const "all_page_widgets","$hmi_elements[@id = $page_all_elements/@id and @id != $page/@id]";

            const "page_relative_widgets",
                "$all_page_widgets[func:is_descendant_path(func:parselabel(@inkscape:label)/widget/path/@value, $desc/path/@value)]";

            // Take closest ancestor in detachable_elements
            // since nested detachable elements are filtered out
            const "required_detachables", 
                """func:sumarized_elements($page_all_elements)/
                   ancestor-or-self::*[@id = $detachable_elements/@id]""";

            |   "«$desc/arg[1]/@value»": {
            |     widget: hmi_widgets["«@id»"],
            |     bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
            if "$desc/path/@value" {
                if "count($desc/path/@index)=0"
                    warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree
            |     page_index: «$desc/path/@index»,
            }
            |     relative_widgets: [
            foreach "$page_relative_widgets" {
            |         hmi_widgets["«@id»"]`if "position()!=last()" > ,`
            }
            |     ],
            |     absolute_widgets: [
            foreach "$all_page_widgets[not(@id = $page_relative_widgets/@id)]" {
            |         hmi_widgets["«@id»"]`if "position()!=last()" > ,`
            }
            |     ],
            |     required_detachables: {
            foreach "$required_detachables" {
            |         "«@id»": detachable_elements["«@id»"]`if "position()!=last()" > ,`
            }
            |     }
            |   }`if "position()!=last()" > ,`
        }
        | }

        |
        | var default_page = "«$default_page»";
        | var svg_root = id("«$svg_root_id»");
        include text svghmi.js
        | //})();
    }

    // template "*", mode="code_from_descs" {
    //     ||
    //     {
    //         var path, role, name, priv;
    //         var id = "«@id»";
    //     ||

    //     /* if label is used, use it as default name */
    //     if "@inkscape:label"
    //         |> name = "«@inkscape:label»";

    //     | /* -------------- */

    //     // this breaks indent, but fixing indent could break string literals
    //     value "substring-after(svg:desc, $mark)";
    //     // nobody reads generated code anyhow...

    //     ||

    //         /* -------------- */
    //         res.push({
    //             path:path,
    //             role:role,
    //             name:name,
    //             priv:priv
    //         })
    //     }
    //     ||
    // }



    function "defs_by_labels" {
        param "labels","''";
        param "mandatory","'yes'";
        param "hmi_element";
        const "widget_type","@type";
        foreach "str:split($labels)" {
            const "name",".";
            const "elt_id","$result_svg_ns//*[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]/@id";
            choose {
                when "not($elt_id)" {
                    if "$mandatory='yes'" {
                        // TODO FIXME error > «$widget_type» widget must have a «$name» element
                        warning > «$widget_type» widget must have a «$name» element
                    }
                    // otherwise produce nothing
                }
                otherwise {
                    |     «$name»_elt: id("«$elt_id»"),
                }
            }
        }
    }


    template "widget[@type='Display']", mode="widget_defs" {
        param "hmi_element";
        |     frequency: 5,
        |     dispatch: function(value) {
        choose {
            when "$hmi_element[self::svg:text]"{
            // TODO : care about <tspan> ?
        |       this.element.textContent = String(value);
            }
            otherwise {
                warning > Display widget as a group not implemented
            }
        }
        |     },

    }
    template "widget[@type='Meter']", mode="widget_defs" {
        param "hmi_element";
        |     frequency: 10,
        labels("needle range");
        optional_labels("value min max");
        |     dispatch: function(value) {
        |         if(this.value_elt)
        |             this.value_elt.textContent = String(value);
        |         let [min,max,totallength] = this.range;
        |         let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min)));
        |         let tip = this.range_elt.getPointAtLength(length);
        |         this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
        |     },
        |     origin: undefined,
        |     range: undefined,
        |     init: function() {
        |         let min = this.min_elt ?
        |                     Number(this.min_elt.textContent) :
        |                     this.args.length >= 1 ? this.args[0] : 0;
        |         let max = this.max_elt ?
        |                     Number(this.max_elt.textContent) :
        |                     this.args.length >= 2 ? this.args[1] : 100;
        |         this.range = [min, max, this.range_elt.getTotalLength()]
        |         this.origin = this.needle_elt.getPointAtLength(0);
        |     },
    }

    def "func:escape_quotes" {
        param "txt";
        // have to use a python string to enter escaped quote
        const "frst", !"substring-before($txt,'\"')"!;
        const "frstln", "string-length($frst)";
        choose {
            when "$frstln > 0 and string-length($txt) > $frstln" {
                result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
            }
            otherwise {
                result "$txt";
            }
        }
    }

    template "widget[@type='Input']", mode="widget_defs" {
        param "hmi_element";
        const "value_elt" {
            optional_labels("value");
        }
        const "have_value","string-length($value_elt)>0";
        value "$value_elt";
        if "$have_value"
        |     frequency: 5,

        |     dispatch: function(value) {

        if "$have_value"
        |         this.value_elt.textContent = String(value);

        |     },
        const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
        |     init: function() {
        if "$edit_elt_id" {
        |         id("«$edit_elt_id»").addEventListener(
        |             "click", 
        |             evt => alert('XXX TODO : Edit value'));
        }
        foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
        |         id("«@id»").addEventListener(
        |             "click", 
        |             evt => {let new_val = change_hmi_value(this.indexes[0], "«func:escape_quotes(@inkscape:label)»");
            if "$have_value"{
        |                     this.value_elt.textContent = String(new_val);
            }
        |                    });
                              /* TODO gray out value until refreshed */
        }
        |     },
    }
    template "widget[@type='Button']", mode="widget_defs" {
    }
    template "widget[@type='Toggle']", mode="widget_defs" {
        |     frequency: 5,
    }
    template "widget[@type='Switch']", mode="widget_defs" {
        param "hmi_element";
        |     frequency: 5,
        |     dispatch: function(value) {
        |         for(let choice of this.choices){
        |             if(value != choice.value){
        |                 choice.elt.setAttribute("style", "display:none");
        |             } else {
        |                 choice.elt.setAttribute("style", choice.style);
        |             }
        |         }
        |     },
        |     init: function() {
        |         // Hello Switch
        |     },
        |     choices: [
        const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
        foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
            const "literal", "regexp:match(@inkscape:label,$regex)[2]";
        |         {
        |             elt:id("«@id»"),
        |             style:"«@style»",
        |             value:«$literal»
        |         }`if "position()!=last()" > ,`
        }
        |     ],
    }
    template "widget[@type='Jump']", mode="widget_defs" {
        param "hmi_element";
        |     on_click: function(evt) {
        |         switch_page(this.args[0], this.indexes[0]);
        |     },
        |     init: function() {
        /* registering event this way doies not "click" through svg:use 
        |     this.element.onclick = evt => switch_page(this.args[0]);
        event must be registered by adding attribute to element instead
        TODO : generalize mouse event handling by global event capture + getElementsAtPoint()
        */
        |         this.element.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click(evt)");
        |     },
    }
}