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 * */ 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 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»":[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_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 ? | 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)"); | }, } }