svghmi/gen_index_xhtml.ysl2
author Edouard Tisserant
Mon, 02 Mar 2020 14:43:21 +0100
branchsvghmi
changeset 2848 c525b1083653
parent 2847 dffade5c83d3
child 2849 bb89a2fbb4e0
permissions -rw-r--r--
SVGHMI: declares pages detachable requirements as an object, to find more efficiently common requirements
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;

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"
            exclude-result-prefixes="ns str regexp exsl func" {

    /* This retrieves geometry obtained through "inkscape -S"
     * already parsed by python and presented as a list of
     * <bbox x="0" y="0" w="42" h="42">
     */
    const "geometry", "ns:GetSVGGeometry()";
    const "hmitree", "ns:GetHMITree()";

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

    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_NODE
        noindex > HMI_PLC_STATUS
        noindex > HMI_CURRENT_PAGE
    }
    const "categories", "exsl:node-set($_categories)";
    const "_indexed_hmitree" apply "$hmitree", mode="index";
    const "indexed_hmitree", "exsl:node-set($_indexed_hmitree)";


    // returns all directly or indirectly refered elements
    def "func:refered_elements" {
        param "elems";
        const "descend", "$elems/descendant-or-self::svg:*";
        const "clones", "$descend[self::svg:use]";
        // TODO optimize using Xpath' id()
        const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
        choose {
            when "$originals" 
                result "$descend | func:refered_elements($originals)";
            otherwise
                result "$descend";
        }
    }

    def "func:intersect_1d" {
        /* it is assumed that a1 > a0 and b1 > b0 */
        param "a0";
        param "a1";
        param "b0";
        param "b1";

        const "d0", "$a0 >= $b0";
        const "d1", "$a1 >= $b1";
        choose {
            when "not($d0) and $d1"
                // b contained in a
                //   a0<b0 b1<a1
                // a +--------+
                // b    +--+
                result "3";
            when "$d0 and not($d1)"
                // a contained in b
                //   b0<a0 a1<b1
                // a    +--+
                // b +--------+
                result "2";
            when "$d0 and $d1 and $a0 < $b1"
                // a and b are overlapped 
                //   b0<a0<b1<a1
                // a    +-----+
                // b +-----+
                result "1";
            when "not($d0) and not($d1) and $b0 < $a1"
                // a and b are overlapped
                //   a0<b0<a1<b1
                // a +-----+
                // b    +-----+
                result "1";
            otherwise
                result "0"; /* no intersection*/
        }
    }

    // returns :
    // 0 - no intersection
    //            .-----.
    //    .-----. |    b|
    //    |     | |     |
    //    |     | '-----'
    //    |a    |
    //    '-----'
    //
    // 1 - overlapping
    //        .-----.
    //    .---|--. b|
    //    |   |  |  |
    //    |   '-----'
    //    |a     |
    //    '------'
    //
    // 2 - overlapping 
    //        .-----.    
    //        |  a  |
    //    .---|-----|---.
    //    |   '-----'   |
    //    | b           |
    //    '-------------'
    //
    // 3 - overlapping 
    //        .-----.    
    //        |  b  |    
    //    .---|-----|---.
    //    |   '-----'   |
    //    | a           |
    //    '-------------'
    //
    // 4 - a contained in b
    //    .-------------.
    //    |   .-----.   |
    //    |   |  a  |   |
    //    |b  '-----'   |
    //    '-------------'
    //
    // 6 - overlapping
    //        .----.    
    //        |   b|    
    //    .---|----|---.
    //    |a  |    |   |
    //    '---|----|---'
    //        '----'
    //
    // 9 - b contained in a
    //    .-------------.
    //    |   .-----.   |
    //    |   |  b  |   |
    //    |a  '-----'   |
    //    '-------------'
    //
    def "func:intersect" {
        param "a";
        param "b";

        const "x_intersect", "func:intersect_1d($a/@x, $a/@x+$a/@w, $b/@x, $b/@x+$b/@w)";

        choose{
            when "$x_intersect != 0"{
                const "y_intersect", "func:intersect_1d($a/@y, $a/@y+$a/@h, $b/@y, $b/@y+$b/@h)";
                result "$x_intersect * $y_intersect";
            }
            otherwise result "0";
        }
    }

    // return overlapping geometry for a given element
    // all intersercting element are returned
    // except groups, that must be contained to be counted in
    const "groups", "/svg:svg | //svg:g";

    def "func:overlapping_geometry" {
        param "elt";
        const "g", "$geometry[@Id = $elt/@id]"; 
        const "candidates", "$geometry[@Id != $elt/@id]";
        result """$candidates[(@Id = $groups/@id and (func:intersect($g, .) = 9)) or 
                              (not(@Id = $groups/@id) and (func:intersect($g, .) > 0 ))]""";
    }

    def "func:all_related_elements" {
        param "page";
        const "page_overlapping_geometry", "func:overlapping_geometry($page)";
        const "page_overlapping_elements", "//svg:*[@id = $page_overlapping_geometry/@Id]";
        const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements)";
        result "$page_sub_elements";
    }

    def "func:required_elements" {
        param "pages"; 
        choose{
            when "$pages"{
                result """func:all_related_elements($pages[1])
                          | func:required_elements($pages[position()!=1])""";
            }otherwise{
                result "/..";
            }
        }
    }

    const "required_elements", 
        """//svg:defs/descendant-or-self::svg:*
           | func:required_elements($hmi_pages)/ancestor-or-self::svg:*""";

    const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]";

    def "func:sumarized_elements" {
        param "elements";
        const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
        const "filled_groups", """$short_list/parent::svg:*[
            not(descendant::*[
                not(self::svg:g) and 
                not(@id = $discardable_elements/@id) and
                not(@id = $short_list/descendant-or-self::*[not(self::svg:g)]/@id)
            ])]""";
        const "groups_to_add", "$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]";
        result "$groups_to_add | $short_list[not(ancestor::svg:g/@id = $filled_groups/@id)]";
    }

    def "func:detachable_elements" {
        param "pages"; 
        choose{
            when "$pages"{
                result """func:sumarized_elements(func:all_related_elements($pages[1]))
                          | func:detachable_elements($pages[position()!=1])""";
            }otherwise{
                result "/..";
            }
        }
    }
    
    const "detachable_elements", "func:detachable_elements($hmi_pages)";
    // const "essential_elements", "$detachable_elements | /svg:svg/svg:defs";
    // const "required_elements", "$essential_elements//svg:* | $essential_elements/ancestor-or-self::svg:*";

    template "*", mode="index" {
        param "index", "0";
        param "parentpath", "''";
        const "content" {
            const "path"
                choose {
                    when "local-name() = 'HMI_ROOT'" > «$parentpath»
                    otherwise > «$parentpath»/«@name»
                }
            choose {
                when "not(local-name() = $categories/noindex)" {
                    xsl:copy {
                        attrib "index" > «$index»
                        attrib "hmipath" > «$path»
                        foreach "@*" xsl:copy;
                    }
                    /* no node expected below value nodes */
                }
                otherwise {
                    apply "*[1]", mode="index"{
                        with "index", "$index";
                        with "parentpath" > «$path»
                    }
                }
            }
        }

        copy "$content";
        apply "following-sibling::*[1]", mode="index" {
            with "index", "$index + count(exsl:node-set($content)/*)";
            with "parentpath" > «$parentpath»
        }
    }

    /* 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
    }

    
    //// Commented out before implementing runtime DOM remove/append on page switch - would have side effect
    ////
    //// /* clone unlinkink until widget for better perf with webkit */
    //// svgtmpl "svg:use", mode="inline_svg" 
    //// {
    ////     g{
    ////         attrib "style" > «@style»
    ////         attrib "transform" > «@transform»
    ////         /* keep same id and label in case it is a widget */
    ////         //attrib "inkscape:label","@inkscape:label";
    ////         attrib "id" > «@id»
    ////         const "targetid","substring-after(@xlink:href,'#')";
    ////         apply "//svg:*[@id = $targetid]", mode="unlink_clone";
    ////     }
    //// }
    //// svgtmpl "@*", mode="unlink_clone" xsl:copy;
    //// svgtmpl "svg:*", mode="unlink_clone" {
    ////     choose {
    ////         when "@id = $hmi_elements/@id" {
    ////             use{
    ////                 attrib "xlink:href" > «concat('#',@id)»
    ////             }
    ////         }
    ////         otherwise {
    ////             xsl:copy apply "@* | node()", mode="unlink_clone";
    ////         }
    ////     }
    //// }

    // template "svg:use/@style", mode="inline_svg"{
    //     attrib "style" > all:initial;
    //     //«.»
    // }

    // template "svg:*[concat('#',@id) = //svg:use/@xlink:href]/@style", mode="inline_svg"{
    //     attrib "style" > all:unset;
    //     //«.»
    // }

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

    /* copy root node and add geometry as comment for a test */
    template "/" { 
        comment > Made with SVGHMI. https://beremiz.org
        /* DEBUG DATA */
        comment {
            apply "$hmi_geometry", mode="testgeo";
        }
        comment {
            apply "$hmitree", mode="testtree";
        }
        comment {
            apply "$indexed_hmitree", mode="testtree";
        }
        comment {
            | Detachable :
            foreach "$detachable_elements"{
                | «@id»
            }
        }
        comment {
            | Discardable :
            foreach "$discardable_elements"{
                | «@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;" {
                apply "svg:svg", mode="inline_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, '@')" {
                path {
                    attrib "value" > «.»
                }
            }
        }

        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";
            | "«@id»": {
            |     type: "«$widget/@type»",
            |     args: [
            foreach "$widget/arg"
            |         "«@value»"`if "position()!=last()" > ,`
            |     ],
            |     indexes: [
            foreach "$widget/path" {
                const "hmipath","@value";
                const "hmitree_match","$indexed_hmitree/*[@hmipath = $hmipath]";
                choose {
                    when "count($hmitree_match) = 0" {
                        warning > No match for path "«$hmipath»" in HMI tree
                    }
                    otherwise {
            |         «$hmitree_match/@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»" : {element: id("«@id»"), parent: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_ids","$page_all_elements[@id = $hmi_elements/@id and @id != $page/@id]/@id";

            const "required_detachables", "func:sumarized_elements($page_all_elements)";

            |     "«$desc/arg[1]/@value»": {
            |         widget: hmi_widgets["«@id»"],
            |         bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
            |         widgets: [
            foreach "$all_page_ids" {
            |             hmi_widgets["«.»"]`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
    //         })
    //     }
    //     ||
    // }


    /**/
    template "bbox", mode="testgeo"{
        | ID: «@Id» x: «@x» y: «@y» w: «@w» h: «@h»
    }

    template "*", mode="testtree"{
        param "indent", "''";
        > «$indent» «local-name()» 
        foreach "@*" > «local-name()»=«.» 
        > \n
        apply "*", mode="testtree" {
            with "indent" value "concat($indent,'>')"
        };
    }
    /**/

    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","$hmi_element//*[@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("value min max needle range");
        | dispatch: function(value) {
        |     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);
        // TODO : deal with transformations between needle and range
        |     this.needle_elt.setAttribute('d', "M "+this.origin.x+","+this.origin.y+" "+tip.x+","+tip.y);
        | },
        | origin: undefined,
        | range: undefined,
        | init: function() {
        |     this.range = [Number(this.min_elt.textContent), Number(this.max_elt.textContent), 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");
        }
        value "$value_elt";
        if "$value_elt"
            | frequency: 5,
        | dispatch: function(value) {
        if "$value_elt"
        |     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)»");
        |                 this.value_elt.textContent = String(new_val);});
                          /* could 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) {
        |     console.log(evt);
        |     switch_page(this.args[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)");
        | },
    }
}