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};
in xsl decl svgtmpl(match, xmlns="http://www.w3.org/2000/svg") alias template;
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 */
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" {
"$hmi_pages[func:parselabel(@inkscape:label)/widget/arg[1]/@value = 'Home']";
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»
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" {
const "descend", "$elems/descendant-or-self::svg:*";
const "clones", "$descend[self::svg:use]";
const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
result "$descend | func:refered_elements($originals)";
def "func:intersect_1d" {
/* it is assumed that a1 > a0 and b1 > b0 */
const "d0", "$a0 >= $b0";
const "d1", "$a1 >= $b1";
result "3"; /* a included in b */
result "2"; /* b included in a */
when "$d0 = $d1 and $b0 < $a1"
result "1"; /* a and b are overlapped */
result "0"; /* no intersection*/
const "x_intersect", "func:intersect_1d($a/@x, $a/@x+$a/@w, $b/@x, $b/@x+$b/@w)";
when "$x_intersect != 0"{
const "y_intersect", "func:intersect_1d($a/@y, $a/@y+$a/@w, $b/@y, $b/@y+$b/@w)";
result "$x_intersect * $y_intersect";
// return overlapping geometry a given element
def "func:overlapping_geometry" {
/* only included groups are returned */
/* all other elemenst are returne when overlapping*/
const "g", "$geometry[@Id = $elt/@id]";
result """$geometry[@Id != $elt/@id and func:intersect(., $g) = 4]""";
def "func:sumarized_elements" {
const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
/* TODO exclude globally discardable elements from group fulfillment check */
const "filled_groups", "$short_list/parent::svg:*[not(descendant::*[not(self::svg:g)][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:all_related_elements" {
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:detachable_elements" {
result """func:sumarized_elements(func:all_related_elements($pages[1]))
| func:detachable_elements($pages[position()!=1])""";
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:*";
const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]";
template "*", mode="index" {
param "parentpath", "''";
when "local-name() = 'HMI_ROOT'" > «$parentpath»
otherwise > «$parentpath»/«@name»
when "not(local-name() = $categories/noindex)" {
attrib "index" > «$index»
attrib "hmipath" > «$path»
/* no node expected below value nodes */
apply "*[1]", mode="index"{
with "parentpath" > «$path»
apply "following-sibling::*[1]", mode="index" {
with "index", "$index + count(exsl:node-set($content)/*)";
with "parentpath" > «$parentpath»
* - copy every attributes
* - copy every sub-elements
template "@* | node()", mode="inline_svg" {
/* use real xsl:copy instead copy-of alias from yslt.yml2 */
xsl:copy apply "@* | node()", mode="inline_svg";
/* TODO filter out globally discardable elements */
/* 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
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"
//// attrib "style" > «@style»
//// attrib "transform" > «@transform»
//// /* keep same id and label in case it is a widget */
//// //attrib "inkscape:label","@inkscape:label";
//// const "targetid","substring-after(@xlink:href,'#')";
//// apply "//svg:*[@id = $targetid]", mode="unlink_clone";
//// svgtmpl "@*", mode="unlink_clone" xsl:copy;
//// svgtmpl "svg:*", mode="unlink_clone" {
//// when "@id = $hmi_elements/@id" {
//// attrib "xlink:href" > «concat('#',@id)»
//// 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 */
comment > Made with SVGHMI. https://beremiz.org
apply "$hmi_geometry", mode="testgeo";
apply "$hmitree", mode="testtree";
apply "$indexed_hmitree", mode="testtree";
foreach "$detachable_elements"{
foreach "$discardable_elements"{
html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" {
body style="margin:0;overflow:hidden;" {
apply "svg:svg", mode="inline_svg";
"HMI:WidgetType:param1:param2@path1@path2"
widget type="WidgetType" {
const "description", "substring-after($label,'HMI:')";
const "_args", "substring-before($description,'@')";
when "$_args" value "$_args";
otherwise value "$description";
const "_type", "substring-before($args,':')";
when "$_type" value "$_type";
const "ast" if "$type" widget {
foreach "str:split(substring-after($args, ':'), ':')" {
const "paths", "substring-after($description,'@')";
foreach "str:split($paths, '@')" {
result "exsl:node-set($ast)";
| var hmi_hash = [«$hmitree/@hash»];
function evaluate_js_from_descriptions() {
const "midmark" > \n«$mark»
apply """//*[contains(child::svg:desc, $midmark) or \
starts-with(child::svg:desc, $mark)]""",2
foreach "$hmi_elements" {
const "widget", "func:parselabel(@inkscape:label)/widget";
| type: "«$widget/@type»",
| "«@value»"`if "position()!=last()" > ,`
const "hmipath","@value";
const "hmitree_match","$indexed_hmitree/*[@hmipath = $hmipath]";
when "count($hmitree_match) = 0" {
warning > No match for path "«$hmipath»" in HMI tree
| «$hmitree_match/@index»`if "position()!=last()" > ,`
| element: document.getElementById("«@id»"),
apply "$widget", mode="widget_defs" with "hmi_element",".";
| }`if "position()!=last()" > ,`
| var heartbeat_index = «$indexed_hmitree/*[@hmipath = '/HEARTBEAT']/@index»;
foreach "$indexed_hmitree/*" {
| /* «@index» «@hmipath» */ "«substring(local-name(), 5)»"`if "position()!=last()" > ,`
const "desc", "func:parselabel(@inkscape:label)/widget";
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 "shorter_list", "func:sumarized_elements($page_all_elements)";
| "«$desc/arg[1]/@value»": {
| widget: hmi_widgets["«@id»"],
| bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
foreach "$all_page_ids" {
| hmi_widgets["«.»"]`if "position()!=last()" > ,`
foreach "$shorter_list" {
| }`if "position()!=last()" > ,`
| var default_page = "«$default_page»";
| var svg_root = document.getElementById("«$svg_root_id»");
// template "*", mode="code_from_descs" {
// var path, role, name, priv;
// /* if label is used, use it as default name */
// |> name = "«@inkscape:label»";
// | /* -------------- */
// // this breaks indent, but fixing indent could break string literals
// value "substring-after(svg:desc, $mark)";
// // nobody reads generated code anyhow...
template "bbox", mode="testgeo"{
| ID: «@Id» x: «@x» y: «@y» w: «@w» h: «@h»
template "*", mode="testtree"{
> «$indent» «local-name()»
foreach "@*" > «local-name()»=«.»
apply "*", mode="testtree" {
with "indent" value "concat($indent,'>')"
function "defs_by_labels" {
param "mandatory","'yes'";
const "widget_type","@type";
foreach "str:split($labels)" {
const "elt_id","$hmi_element//*[@inkscape:label=$name][1]/@id";
// TODO FIXME error > «$widget_type» widget must have a «$name» element
warning > «$widget_type» widget must have a «$name» element
// otherwise produce nothing
| «$name»_elt: document.getElementById("«$elt_id»"),
template "widget[@type='Display']", mode="widget_defs" {
| dispatch: function(value) {
when "$hmi_element[self::svg:text]"{
// TODO : care about <tspan> ?
| this.element.textContent = String(value);
warning > Display widget as a group not implemented
template "widget[@type='Meter']", mode="widget_defs" {
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);
| 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" {
// have to use a python string to enter escaped quote
const "frst", !"substring-before($txt,'\"')"!;
const "frstln", "string-length($frst)";
when "$frstln > 0 and string-length($txt) > $frstln" {
result !"concat($frst,'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
template "widget[@type='Input']", mode="widget_defs" {
optional_labels("value");
| dispatch: function(value) {
| this.value_elt.textContent = String(value);
const "edit_elt_id","$hmi_element/*[@inkscape:label='edit'][1]/@id";
| document.getElementById("«$edit_elt_id»").addEventListener(
| evt => alert('XXX TODO : Edit value'));
foreach "$hmi_element/*[regexp:test(@inkscape:label,'^[=+\-].+')]" {
| document.getElementById("«@id»").addEventListener(
| 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" {
template "widget[@type='Switch']", mode="widget_defs" {
| dispatch: function(value) {
| for(let choice of this.choices){
| if(value != choice.value){
| choice.elt.setAttribute("style", "display:none");
| choice.elt.setAttribute("style", choice.style);
const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+)(#.*)?$'"!;
foreach "$hmi_element/*[regexp:test(@inkscape:label,$regex)]" {
const "literal", "regexp:match(@inkscape:label,$regex)[2]";
| elt:document.getElementById("«@id»"),
| }`if "position()!=last()" > ,`
template "widget[@type='Jump']", mode="widget_defs" {
| on_click: function(evt) {
| switch_page(this.args[0]);
/* 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)");