svghmi/inline_svg.ysl2
author Edouard Tisserant <edouard.tisserant@gmail.com>
Tue, 01 Aug 2023 21:04:00 +0200
changeset 3840 c2b6354f036f
parent 3690 f41733be17a8
permissions -rw-r--r--
Tests: Add minimal SVGHMI test
// inline_svg.ysl2
//
// Produce Inline SVG element of resulting XHTML page.

// Since stylesheet output namespace is xhtml, templates that output svg have to be explicitely declared as such 
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;


// Identity template :
//  - copy every attributes 
//  - copy every sub-elements

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

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
}

// remove i18n markers, so that defs_by_labels can find text elements
svgtmpl "svg:text/@inkscape:label[starts-with(., '_')]", mode="inline_svg" {
    attrib "{name()}" > «substring(., 2)»
}

// remove "reference" and "frame" rectangles
svgtmpl "svg:rect[@inkscape:label='reference' or @inkscape:label='frame']", mode="inline_svg" {
    // nothing
}

svgtmpl "svg:g[svg:rect/@inkscape:label='frame']", mode="inline_svg" {
    const "reference_rect","(../svg:rect | ../svg:g/svg:rect)[@inkscape:label='reference']";
    const "frame_rect","svg:rect[@inkscape:label='frame']";
    const "offset","func:offset($frame_rect, $reference_rect)";

    xsl:copy {
        attrib "svghmi_x_offset" value "$offset/vector/@x";
        attrib "svghmi_y_offset" value "$offset/vector/@y";
        apply "@* | node()", mode="inline_svg";
    }
}

////// 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 "targets_not_to_unlink", "$hmi_lists/descendant-or-self::svg:*";
const "to_unlink", "$hmi_widgets/descendant-or-self::svg:use";

def "func:is_unlinkable" {
    param "targetid";
    param "eltid";
    result "$eltid = $to_unlink/@id and not($targetid = $targets_not_to_unlink/@id)";
}

svgtmpl "svg:use", mode="inline_svg"{
    const "targetid","substring-after(@xlink:href,'#')";
    choose {
        when "func:is_unlinkable($targetid, @id)" {
            call "unlink_clone" {
                with "targetid", "$targetid";
            }
        }
        otherwise xsl:copy apply "@*", 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
    name > id
}
const "excluded_use_attrs","exsl:node-set($_excluded_use_attrs)";

const "_merge_use_attrs" {
    name > transform
    name > style
}
const "merge_use_attrs","exsl:node-set($_merge_use_attrs)";

svgfunc "unlink_clone"{
    param "targetid";
    param "seed","''";
    const "target", "//svg:*[@id = $targetid]";
    const "seeded_id" choose {
        when "string-length($seed) > 0" > «$seed»_«@id»
        otherwise value "@id";
    }
    g{
        attrib "id" value "$seeded_id";
        attrib "original" value "@id";

        choose {
            when "$target[self::svg:g]" {
                foreach "@*[not(local-name() = $excluded_use_attrs/name | $merge_use_attrs)]"
                    attrib "{name()}" > «.»

                if "@style | $target/@style"
                    attrib "style" {
                        > «@style»
                        if "@style and $target/@style" > ;
                        > «$target/@style»
                    }

                if "@transform | $target/@transform"
                    attrib "transform" {
                        > «@transform»
                        if "@transform and $target/@transform" >  
                        > «$target/@transform»
                    }

                apply "$target/*", mode="unlink_clone"{
                    with "seed","$seeded_id";
                }
            }
            otherwise {
                // include non excluded attributes
                foreach "@*[not(local-name() = $excluded_use_attrs/name)]"
                    attrib "{name()}" > «.»

                apply "$target", mode="unlink_clone"{
                    with "seed","$seeded_id";
                }
            }
        }
    }
}

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

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

svgtmpl "svg:use", mode="unlink_clone" {
    param "seed";
    const "targetid","substring-after(@xlink:href,'#')";
    choose {
        when "func:is_unlinkable($targetid, @id)" {
            call "unlink_clone" {
                with "targetid", "$targetid";
                with "seed","$seed";
            }
        }
        otherwise xsl:copy apply "@*", mode="unlink_clone" {
            with "seed","$seed";
        }
    }
}

// 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_widgets/@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 "result_svg" apply "/", mode="inline_svg";
const "result_svg_ns", "exsl:node-set($result_svg)";

emit "preamble:inline-svg" {
    | const xmlns = "http://www.w3.org/2000/svg";
    | let id = document.getElementById.bind(document);
    | var svg_root = id("«$svg/@id»");
}

emit "debug:clone-unlinking" {
    |
    | Unlinked :
    foreach "$to_unlink"{
        | «@id»
    }
    | Not to unlink :
    foreach "$targets_not_to_unlink"{
        | «@id»
    }
}