diff --git a/ltk 01 -project starter/README.md b/ltk 01 -project starter/README.md
new file mode 100644
index 0000000..d6285cf
--- /dev/null
+++ b/ltk 01 -project starter/README.md
@@ -0,0 +1,22 @@
+When developing an application which is larger than a demo, its useful to have a better structure.
+
+This series is about presenting a better project-based structure.
+
+# Part 1:
+
+## Pyodide (full python) or Micropython
+We want to delay the decision of choosing between these two as long as possible.
+To this end the index.html has both possible with Pyodide initially disabled.
+
+## Loading animation
+The very first time a user hits your page, there will be a delay.
+This delay will always be longer for Pyodide than for Micropython.
+The index.html has a preloading section for required css and a modal dialog.
+
+## Terminal
+When initially developing it's often useful to have the python terminal available.
+The terminal takes up a bit of room but it can be resized.
+
+## Initial user interaction
+Dom manipulation is supported via pyscript.web
+Next steps are to go to the ltk for all screen UI
diff --git a/ltk 01 -project starter/index.html b/ltk 01 -project starter/index.html
new file mode 100644
index 0000000..db2ceeb
--- /dev/null
+++ b/ltk 01 -project starter/index.html
@@ -0,0 +1,90 @@
+
+
+
+ Newtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ltk 01 -project starter/main.css b/ltk 01 -project starter/main.css
new file mode 100644
index 0000000..3869831
--- /dev/null
+++ b/ltk 01 -project starter/main.css
@@ -0,0 +1,3 @@
+.css-class1 {
+ background: #f8f6e7;
+}
\ No newline at end of file
diff --git a/ltk 01 -project starter/main.py b/ltk 01 -project starter/main.py
new file mode 100644
index 0000000..31799f7
--- /dev/null
+++ b/ltk 01 -project starter/main.py
@@ -0,0 +1,34 @@
+from pyscript.web import page, div, button, span, br
+from pyscript import when, window, config
+MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython
+
+if '__terminal__' in locals():
+ __terminal__.resize(60, 10)
+
+def add_ui(toplevelpage):
+ toplevelpage.append(
+ div(
+ button(span("Hello! "), span("World!")),
+ br(),
+ button("Click me!", id="my-button"),
+ classes=["css-class1", "css-class2"],
+ style={"border": "2px dashed red"}
+ ))
+
+def my_button_click_handler(event):
+ print("The button has been clicked!")
+
+if __name__ == "__main__":
+ print(f"Printing to Terminal: ({"mpy" if MICROPYTHON else "pyodide"})")
+ add_ui(page)
+ # UI built
+ when("click", "#my-button", handler=my_button_click_handler)
+ # explain
+ explanation = ["Index example:","- Specify micropython or pyodide in index.html",
+ " - two manual changes needed in index.html to swap",
+ "- Add a loading animation",
+ "- Include a resized Terminal for manual debugging",
+ "- basic pyscript.web based dom manipulation"
+ ]
+ print("\n".join(explanation))
+
\ No newline at end of file
diff --git a/ltk 01 -project starter/pyscript_mpy.toml b/ltk 01 -project starter/pyscript_mpy.toml
new file mode 100644
index 0000000..9c34853
--- /dev/null
+++ b/ltk 01 -project starter/pyscript_mpy.toml
@@ -0,0 +1 @@
+name = "Newtitle"
diff --git a/ltk 01 -project starter/pyscript_py.toml b/ltk 01 -project starter/pyscript_py.toml
new file mode 100644
index 0000000..9c34853
--- /dev/null
+++ b/ltk 01 -project starter/pyscript_py.toml
@@ -0,0 +1 @@
+name = "Newtitle"
diff --git a/ltk 02 -project starter/README.md b/ltk 02 -project starter/README.md
new file mode 100644
index 0000000..aa3f604
--- /dev/null
+++ b/ltk 02 -project starter/README.md
@@ -0,0 +1,18 @@
+When developing an application which is larger than a demo, its useful to have a better structure.
+
+This series is about presenting a better project-based structure.
+
+# Part 2:
+Building on ideas described in Part1.
+ - Pyodide (full python) or Micropython
+ - "Loading" animation
+
+## Initial user interaction
+We are now using the Ltk for Dom manipulation and all screen UI.
+Ltk has jQuery base, so can also use that approach for Selectors and dom manipulation
+
+## Terminal
+The Ltk allows us to move the terminal into a sliding section.
+This means we can minimise the terminal on-screen while still having full access.
+
+
diff --git a/ltk 02 -project starter/index.html b/ltk 02 -project starter/index.html
new file mode 100644
index 0000000..29dff4e
--- /dev/null
+++ b/ltk 02 -project starter/index.html
@@ -0,0 +1,96 @@
+
+
+
+ Newtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ltk 02 -project starter/main.css b/ltk 02 -project starter/main.css
new file mode 100644
index 0000000..3fb2c0f
--- /dev/null
+++ b/ltk 02 -project starter/main.css
@@ -0,0 +1,8 @@
+.description {
+ font-size: 1.0rem;
+ font-style: light;
+ border: 2px dashed red;
+}
+#graphic {
+ border: 1px dashed #0d720d;
+}
\ No newline at end of file
diff --git a/ltk 02 -project starter/main.py b/ltk 02 -project starter/main.py
new file mode 100644
index 0000000..4c4190a
--- /dev/null
+++ b/ltk 02 -project starter/main.py
@@ -0,0 +1,93 @@
+#imports
+import ltk
+import svg as SVG
+import random
+from pyscript import config
+MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython
+
+if '__terminal__' in locals():
+ __terminal__.resize(60, 30)
+
+# for SVG
+solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"}
+
+def create_svg_box(size=5):
+ overall = 100
+ box_size = 100 / size
+ highlight_svg = SVG.svg(width=overall, height=overall,
+ preserveAspectRatio="xMidYMid meet",
+ viewBox=f"0 0 {overall+2} {overall+2}")
+ # Transform the group to move its children
+ grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style)
+ highlight_svg.appendChild(grp)
+ grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size))
+ #
+ return highlight_svg
+
+def button_click_handler(event):
+ print("The button has been clicked, the svg transformed")
+ # move the svg child
+ x,y = random.randrange(0, 100-20), random.randrange(0, 100-20)
+ ltk.find("#highlight").attr("transform", f"translate({x} {y})")
+
+class UI_widget(object):
+ """
+ Top level object that shows App.
+ """
+
+ def __init__(self):
+ self.explanation = ["Ltk project example:","- Swap between micropython or pyodide in index.html",
+ "- Everything from starter_01 but using the Ltk for UI instead",
+ "- 'Ltk' used instead of direct dom manipulation",
+ "- Ltk has jQuery base, so can also use that approach for Selectors and dom manipulation",
+ "- The Terminal is in a top level slider. Try moving it out of the way",
+ "- SVG is included for more complex UIs"
+ ]
+
+ def create(self):
+ info_paragraph = ltk.Paragraph(" ".join(self.explanation)).attr("id","explanation").addClass("description")
+ return (ltk.VBox(
+ ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way
+ ltk.Div(
+ # put your UI in here
+ # - can easily just remove these dummy panes when you no longer need the terminal
+ ltk.Div(
+ ltk.Button("Click me!", button_click_handler).attr("id","my-button")
+ ),
+ ltk.Div().attr("id", "graphic"),
+ # overall description
+ info_paragraph
+ ).attr("id", "mydiv").css("border", "2px solid red"),
+
+ # For the terminal
+ ltk.Div().attr("id", "forterm").css("border", "2px solid green"),
+ "Temp-split-pane"
+ )
+ ))
+
+
+if __name__ == "__main__":
+ w = UI_widget()
+ widget = w.create()
+ widget.appendTo(ltk.window.document.body)
+
+ # move terminal to sliding pane using one of these methods:
+ # - the usual Javascript mechanism
+ term = ltk.window.document.getElementsByTagName("py-terminal")[0]
+ term_box = ltk.window.document.getElementById("forterm")
+ # - OR
+ # Using ltk.find - jquery style selectors and python lists
+ term = ltk.find("py-terminal")[0]
+ term_box = ltk.find("#forterm")[0]
+ #
+ term_box.appendChild(term)
+ # - OR
+ # Using the jQuery interface with all Jquery chaining flexibility
+ ltk.jQuery("#forterm")[0].append(ltk.jQuery("py-terminal")[0])
+ # remind us of py/mpy
+ print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})")
+ # svg insert into Div
+ somesvg = create_svg_box()
+ ltk.find("#graphic").html(somesvg.outerHTML)
+
+
\ No newline at end of file
diff --git a/ltk 02 -project starter/pyscript_mpy.toml b/ltk 02 -project starter/pyscript_mpy.toml
new file mode 100644
index 0000000..22fdb2e
--- /dev/null
+++ b/ltk 02 -project starter/pyscript_mpy.toml
@@ -0,0 +1,12 @@
+name = "Newtitle"
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+"./svg.py" = ""
\ No newline at end of file
diff --git a/ltk 02 -project starter/pyscript_py.toml b/ltk 02 -project starter/pyscript_py.toml
new file mode 100644
index 0000000..5388c0f
--- /dev/null
+++ b/ltk 02 -project starter/pyscript_py.toml
@@ -0,0 +1,9 @@
+name = "Newtitle"
+
+packages = [ "pyscript-ltk==0.2.20"]
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+"./svg.py" = ""
\ No newline at end of file
diff --git a/ltk 03 -project starter/README.md b/ltk 03 -project starter/README.md
new file mode 100644
index 0000000..bf42d7d
--- /dev/null
+++ b/ltk 03 -project starter/README.md
@@ -0,0 +1,20 @@
+When developing an application which is larger than a demo, its useful to have a better structure.
+
+This series is about presenting a better project-based structure.
+
+# Part 3:
+Building on ideas described in Parts 1,2,3.
+ - Pyodide (full python) or Micropython
+ - "Loading" animation
+
+Now adding:
+## Reactive UI
+The Ltk has a Reaactive parameter capability where named instance variables become reactive in the UI.
+As the user changes the UI, the python variable will update and vice versa.
+There is a simple mechanism to manage race conditions when changing one reactive variable affects any number of other reactive variables.
+
+## Interactive SVG controls
+An SVG module has been added so SVG style UI can be added and full interaction is available.
+Either by using transforms to existing SVG elements or replacing the SVG in whole or in part.
+
+In this demo we transform an svg rect to a random position when the button is clicked.
\ No newline at end of file
diff --git a/ltk 03 -project starter/index.html b/ltk 03 -project starter/index.html
new file mode 100644
index 0000000..29dff4e
--- /dev/null
+++ b/ltk 03 -project starter/index.html
@@ -0,0 +1,96 @@
+
+
+
+ Newtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ltk 03 -project starter/main.css b/ltk 03 -project starter/main.css
new file mode 100644
index 0000000..a4bff9c
--- /dev/null
+++ b/ltk 03 -project starter/main.css
@@ -0,0 +1,23 @@
+.description {
+ font-size: 1.0rem;
+ font-style: light;
+ border: 2px dashed #e232ca;
+}
+.basetext {
+ font-size: 1.4rem;
+ padding: 3px;
+}
+.short {
+ width: 60px;
+ align-self: center;
+ padding: 3px;
+}
+#graphic {
+ border: 1px dashed #0d720d;
+}
+
+/* ltk overrides */
+.ltk-input {
+ font-size: 1.4rem;
+ padding: 3px;
+}
\ No newline at end of file
diff --git a/ltk 03 -project starter/main.py b/ltk 03 -project starter/main.py
new file mode 100644
index 0000000..1a8077c
--- /dev/null
+++ b/ltk 03 -project starter/main.py
@@ -0,0 +1,150 @@
+#imports
+import ltk
+import svg as SVG
+import random
+from pyscript import config
+MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython
+
+if '__terminal__' in locals(): # resize terminal if started in index.html
+ __terminal__.resize(60, 10)
+
+# globals
+g_widget = None # Hold ref to widget for nested components
+# for SVG
+solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"}
+
+def create_svg_box(size=5):
+ overall = 100
+ box_size = 100 / size
+ highlight_svg = SVG.svg(width=overall, height=overall,
+ preserveAspectRatio="xMidYMid meet",
+ viewBox=f"0 0 {overall+2} {overall+2}")
+ # Transform the group to move its children
+ grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style)
+ highlight_svg.appendChild(grp)
+ grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size))
+ #
+ return highlight_svg
+
+## Events
+def button_click_handler(event):
+ print("The button has been clicked, the svg transformed")
+ print(f"- checkbox clicked {g_widget.params.count_check_ticks} times.")
+ # move the svg child
+ x,y = random.randrange(0, 100-20), random.randrange(0, 100-20)
+ ltk.find("#highlight").attr("transform", f"translate({x} {y})")
+
+
+### Reactive UI variables and behaviour
+class Reactive_params(ltk.Model):
+ """
+ """
+ # These variables are all reactive
+ check_me = False
+ Drawin = 12 # Can use Drawin: int = 12 but python runtime does not check
+ Draw_label = "Subtract 2"
+
+ def __init__(self):
+ """
+ variables instanced here will not be reactive (only the classvars above)
+ but its a good place to put all the other variables your widget needs.
+ """
+ super().__init__()
+ self.inhibit_update = False
+ self.count_check_ticks = 0 # as a useless example
+
+ def changed(self, name, value):
+ """
+ Called whenever a UI element is changed by user.
+ - name is always a string.
+ Do whatever changes are required in here
+ Also invoked for internal changes
+ - inhibit_update is used to prevent needless rippling
+ """
+ #print("- Change requested:",name,value)
+ if not self.inhibit_update:
+ self.inhibit_update = True # inhibit rippling calls while we update
+ print("Changing:", name, value)
+ if name == "check_me":
+ # changing another reactive value does not ripple because of global inhibit var
+ if self.check_me:
+ self.Draw_label = "Add 2 extra"
+ self.Drawin -= 2
+ else:
+ self.Draw_label = "Subtract 2"
+ self.Drawin += 2
+ # local iv
+ self.count_check_ticks += 1
+ # reset ripple updates
+ self.inhibit_update = False
+
+
+### UI
+class UI_widget(object):
+ """
+ Top level object that shows App.
+ """
+
+ def __init__(self):
+ self.params = Reactive_params()
+ self.explanation = ["Ltk project example:",
+ "- Everything from starter_02",
+ "- Adding Reactive UI components",
+ "(Use ctrl-shift while moving the mouse over ltk UI elements for descriptions.)"
+ ]
+
+ def label_box(self):
+ """
+ Can make parts of the UI like this
+ """
+ return (ltk.HBox(
+ ltk.Label(self.params.Draw_label),
+ ltk.Checkbox(self.params.check_me).attr("id", "check_1")
+ ).css("border","1px dashed cyan")
+ )
+
+ def create(self):
+ """
+ Return the UI structure
+ """
+ info_paragraph = ltk.Paragraph(" ".join(self.explanation)).attr("id","explanation").addClass("description")
+ return (
+ ltk.VBox(
+ ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way
+ ltk.Div( # put your UI in here
+ # - can easily just remove these dummy panes when you no longer need the terminal
+ ltk.VBox(
+ ltk.Button("Click me!", button_click_handler).attr("id","my-button"),
+ self.label_box(),
+ ltk.HBox(
+ ltk.Label("Draw-in").addClass("basetext"),
+ ltk.Input(self.params.Drawin).attr("type","number").attr("id","drawin").addClass("short")
+ )
+ ),
+ ltk.Div().attr("id", "graphic"),
+ # overall description
+ info_paragraph
+ ).attr("id", "mydiv").css("border", "2px solid red"),
+ # Other side for the terminal
+ ltk.Div().attr("id", "forterm").css("border", "2px solid green"),
+ "Temp-split-pane"
+ )
+ )
+ )
+
+
+if __name__ == "__main__":
+ g_widget = UI_widget()
+ widget = g_widget.create()
+ widget.appendTo(ltk.window.document.body) # ui is now running
+ #
+ # move terminal to sliding pane
+ term = ltk.find("py-terminal")[0]
+ term_box = ltk.find("#forterm")[0]
+ term_box.appendChild(term)
+ #
+ print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})")
+ # svg insert into Div
+ somesvg = create_svg_box()
+ ltk.find("#graphic").html(somesvg.outerHTML)
+
\ No newline at end of file
diff --git a/ltk 03 -project starter/pyscript_mpy.toml b/ltk 03 -project starter/pyscript_mpy.toml
new file mode 100644
index 0000000..22fdb2e
--- /dev/null
+++ b/ltk 03 -project starter/pyscript_mpy.toml
@@ -0,0 +1,12 @@
+name = "Newtitle"
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+"./svg.py" = ""
\ No newline at end of file
diff --git a/ltk 03 -project starter/pyscript_py.toml b/ltk 03 -project starter/pyscript_py.toml
new file mode 100644
index 0000000..f32e3b2
--- /dev/null
+++ b/ltk 03 -project starter/pyscript_py.toml
@@ -0,0 +1,9 @@
+name = "Newtitle"
+
+packages = [ "pyscript-ltk==0.2.30"]
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+"./svg.py" = ""
diff --git a/ltk 03 -project starter/svg.py b/ltk 03 -project starter/svg.py
new file mode 100644
index 0000000..05c94ed
--- /dev/null
+++ b/ltk 03 -project starter/svg.py
@@ -0,0 +1,179 @@
+"""
+A rewrite of Brython's SVG module, to remove JavaScript / document related
+interactions (so this can be used within a web worker, where document is not
+conveniently available).
+
+Author: Nicholas H.Tollervey (ntollervey@anaconda.com)
+Based on original work by: Romain Casati
+Mods for class,todom from Neon22(https://github.com/Neon22)
+
+License: GPL v3 or higher.
+"""
+from pyscript import document
+
+def todom(node):
+ """
+ Get back entity we can jam into the dom live.
+ - If your inserted svg is being ignored then use this as a wrapper before inserting
+ """
+ svgnode = document.createElementNS('http://www.w3.org/2000/svg', node.tagName)
+ for a in node.attributes:
+ svgnode.setAttributeNS(None,a, node.attributes[a])
+ return svgnode
+
+
+class Node:
+ """
+ Represents a node in the DOM.
+ """
+
+ def __init__(self, **kwargs):
+ self._node = kwargs
+ self.parent = kwargs.get("parent")
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML.
+ """
+ return NotImplemented
+
+
+class ElementNode(Node):
+ """
+ An element defined by a tag, may have attributes and children.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.tagName = kwargs["tagName"]
+ self.attributes = kwargs.get("attributes", {})
+ self.value = kwargs.get("value")
+ self.childNodes = []
+
+ def appendChild(self, child):
+ """
+ Add a child node to the children of this node. Using DOM API naming
+ conventions.
+ """
+ child.parent = self
+ self.childNodes.append(child)
+
+ def setAttribute(self, key, value):
+ """
+ Sets an attribute on the node.
+ """
+ self.attributes[key] = value
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML. Using DOM API
+ naming conventions.
+ """
+ result = "<" + self.tagName
+ for attr, val in self.attributes.items():
+ result += " " + attr + '="' + str(val) + '"'
+ result += ">"
+ result += self.innerHTML
+ result += "" + self.tagName + ">"
+ return result
+
+ @property
+ def innerHTML(self):
+ """
+ Get a string representation of the element's inner HTML. Using DOM API
+ naming conventions.
+ """
+ result = ""
+ for child in self.childNodes:
+ result += child.outerHTML
+ return result
+
+
+class TextNode(Node):
+ """
+ Textual content inside an ElementNode.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.nodeValue = kwargs.get("nodeValue")
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML.
+ """
+ return self.nodeValue
+
+
+_svg_ns = "http://www.w3.org/2000/svg"
+_xlink_ns = "http://www.w3.org/1999/xlink"
+
+
+def _tag_func(tag):
+ def func(*args, **kwargs):
+ node = ElementNode(tagName=tag)
+ # this is mandatory to display svg properly
+ if tag == "svg":
+ node.setAttribute("xmlns", _svg_ns)
+ for arg in args:
+ if isinstance(arg, (str, int, float)):
+ arg = TextNode(nodeValue=str(arg))
+ node.appendChild(arg)
+ for key, value in kwargs.items():
+ key = key.lower()
+ if key[0:2] == "on":
+ # Ignore event handlers within the SVG. This shouldn't happen.
+ pass
+ elif key == "style":
+ node.setAttribute(
+ "style", "; ".join(f"{k}: {v}" for k, v in value.items())
+ )
+ elif key == "_class":
+ node.setAttribute(
+ "class", str(value))
+ elif value is not False:
+ node.setAttribute(key.replace("_", "-"), str(value))
+ return node
+
+ return func
+
+
+a = _tag_func("a")
+altGlyph = _tag_func("altGlyph")
+altGlyphDef = _tag_func("altGlyphDef")
+altGlyphItem = _tag_func("altGlyphItem")
+animate = _tag_func("animate")
+animateColor = _tag_func("animateColor")
+animateMotion = _tag_func("animateMotion")
+animateTransform = _tag_func("animateTransform")
+circle = _tag_func("circle")
+clipPath = _tag_func("clipPath")
+color_profile = _tag_func("color_profile")
+cursor = _tag_func("cursor")
+defs = _tag_func("defs")
+desc = _tag_func("desc")
+ellipse = _tag_func("ellipse")
+feBlend = _tag_func("feBlend")
+foreignObject = _tag_func("foreignObject")
+g = _tag_func("g")
+image = _tag_func("image")
+line = _tag_func("line")
+linearGradient = _tag_func("linearGradient")
+marker = _tag_func("marker")
+mask = _tag_func("mask")
+path = _tag_func("path")
+pattern = _tag_func("pattern")
+polygon = _tag_func("polygon")
+polyline = _tag_func("polyline")
+radialGradient = _tag_func("radialGradient")
+rect = _tag_func("rect")
+set = _tag_func("set")
+stop = _tag_func("stop")
+svg = _tag_func("svg")
+text = _tag_func("text")
+tref = _tag_func("tref")
+tspan = _tag_func("tspan")
+use = _tag_func("use")
\ No newline at end of file
diff --git a/ltk 04 -project starter/README.md b/ltk 04 -project starter/README.md
new file mode 100644
index 0000000..615b8c6
--- /dev/null
+++ b/ltk 04 -project starter/README.md
@@ -0,0 +1,27 @@
+When developing an application which is larger than a demo, its useful to have a better structure.
+
+This series is about presenting a better project-based structure.
+
+# Part 4:
+Building on ideas described in Parts 1,2,3.
+ - Pyodide (full python) or Micropython
+ - "Loading" animation
+ - Reactive UI
+ - Interactive SVG controls
+
+## Single File Components
+We are now building a UI element from a separate component file.
+This means you can construct standalone UI components to include when needed.
+ - Making single file components that can be reused is critical for large projects.
+
+In the index.html - you can uncomment script lines to run:
+ - the base module that is imported 'util_units.py' which runs a test,
+ - the single file component (with tests) 'dimension_component.py' which also tests/demonstrates itself,
+ - or the main.py which uses the single file component twice
+
+## All events under Reactive (Model) class
+Moving all toplevel events under the Model class (which enables reactivity) means we can use this as a single file component also.
+This enables anything to be used in this way.
+The testing under '__main__' means a component can be tested/checked on its own and then used as a component elsewhere without further modification.
+
+(You can use ctrl-shift while moving the mouse over ltk UI elements for descriptions of those elements.)
\ No newline at end of file
diff --git a/ltk 04 -project starter/components/dimension_component.py b/ltk 04 -project starter/components/dimension_component.py
new file mode 100644
index 0000000..4877352
--- /dev/null
+++ b/ltk 04 -project starter/components/dimension_component.py
@@ -0,0 +1,124 @@
+# Dimension Component using ltk's reactive MVP model
+
+import ltk
+from util_units import *
+
+# Debug
+def print(*args):
+ ltk.find("body").append(" ".join(str(a) for a in args), " ")
+if '__terminal__' in locals(): # resize terminal if started in index.html
+ __terminal__.resize(60, 6)
+
+class DC_params(ltk.Model):
+ """
+ Reactive UI parameters
+ """
+ measure: str = "250yd"
+ units: str = "yd"
+ altunits: str = "229m"
+
+ def __init__(self, parent, defaults=["20","wpi","wpcm"], inch_base=False):
+ super().__init__()
+ self.parent = parent # not strictly required at this child level but
+ # needed for UI components with operationally dependent children.
+ #
+ self.defaults = defaults
+ self.inch_base = inch_base
+ self.inhibit_update = False
+ #
+ self.set_to_defaults()
+
+ def set_to_defaults(self):
+ self.measure = self.defaults[0] + self.defaults[1]
+ self.units = self.defaults[1]
+ self.altunits = convert_imp(int(self.defaults[0]), self.defaults[2], self.inch_base)
+
+ def changed(self, name, value):
+ """
+ For this component:
+ - toggle units btwn metric/imperial
+ - allow overiding units
+ - allow numeric only or numeric+units entry
+ - show the alt units to be helpful eh
+ """
+ #print(f"DC:Change req: {name} to {value}")
+ if not self.inhibit_update:
+ #print(f"DC:Changing: {name} to {value}")
+ self.inhibit_update = True # inhibit rippling calls while we update
+ if name == "measure":
+ self.update_measure(value)
+ elif name == "units":
+ self.update_units(value)
+ # reset ripple updates
+ self.inhibit_update = False
+ #else:
+ # print("- DC:Skipping change")
+
+ def update_measure(self, value):
+ num, unit = parse_units(value)
+ if num and num > -1:
+ # got a valid measure but maybe no units
+ if not unit:
+ # use on-screen units
+ units = f"{self.units}"
+ self.measure = f"{format_nice(num)}{units}"
+ if units in metric_units:
+ num = num * convert_factors[units]
+ self.altunits = convert_imp(num, unit_swaps[units], self.inch_base)
+ else: # unit from measure
+ if unit not in convert_factors.keys():
+ unit = self.defaults[1]
+ self.units = unit
+ usnum,_ = parse_to_US(value)
+ self.measure = convert_imp(usnum, unit)
+ self.altunits = convert_imp(usnum, unit_swaps[unit])
+ else: # not valid so set default
+ self.set_to_defaults()
+
+ def update_units(self, newunit):
+ num, altunits = parse_units(self.altunits)
+ usnum, _ = parse_to_US(self.measure)
+ if altunits == newunit: # straight swap
+ self.update_measure(f"{num}{newunit}")
+ elif newunit in unit_swaps: # convert to newunit
+ self.update_measure(convert_imp(usnum, newunit))
+ else: # not a unit
+ self.update_measure(f"{usnum}{newunit}") # will be defaulted
+
+
+class Component(object):
+ """
+ Create an instance using: Component(label, defaults, force_inches)
+ Add to UI by using: .create()
+ Read/write values by using .params.name_of_var
+ - Default is [value, unit, altunit] all strings
+ """
+ def __init__(self, label="Foo", defaults=["250","yd","m"], inch_base=False):
+ self.label = label
+ self.params = DC_params(self, defaults=defaults, inch_base=inch_base)
+
+ def toggle_units(self, event, var):
+ self.params.changed(var.name, unit_swaps[f"{var}"])
+
+ def create(self):
+ """
+ Present the value, units as buttons and
+ altunits as a label
+ """
+ return (
+ ltk.VBox(
+ ltk.Label(self.label).addClass("Item paramtitle"),
+ ltk.HBox(
+ ltk.Input(self.params.measure).addClass("measure"),
+ ltk.Input(self.params.units).addClass("measure short").on("click", ltk.proxy(lambda event: self.toggle_units(event, self.params.units))),
+ ltk.Label(self.params.altunits).addClass("measure altlabel noborder")
+ )
+ ).addClass("param")
+ )
+
+# standalone development/examination
+if __name__ == "__main__":
+ w = Component()
+ widget = w.create()
+ widget.appendTo(ltk.window.document.body)
+ ltk.find(".param").css("border 2px dashed red")
\ No newline at end of file
diff --git a/ltk 04 -project starter/components/util_units.py b/ltk 04 -project starter/components/util_units.py
new file mode 100644
index 0000000..34b4f5d
--- /dev/null
+++ b/ltk 04 -project starter/components/util_units.py
@@ -0,0 +1,118 @@
+
+# Here we assume all calcs are done in yds/in
+# but we want to present unit agnostically.
+# - all epi/wpi,ppi are in delivered in inches
+# - all yds/ft/in/m/cm/mm are delivered in yds
+# E.g. "3epcm" becomes "7.5epi", "1ft" becomes "0.333yds"
+
+import re
+
+# convert value in units to result in yards (or inch for epi/ppi/epcm) (use multiply)
+convert_factors = {"yd": 1, "yds": 1, "ft": 0.333333, "in": 0.027777777,
+ "m": 1.09361, "cm": 0.0109361, "mm": 0.00109361,
+ # epi are in inches
+ "wpi": 1, "epi": 1, "ppi": 1, "wpcm": 2.54, "epcm": 2.54, "ppcm": 2.54}
+
+yard_based = ["yd", "m"]
+unit_swaps = {"in":"cm", "cm":"in",
+ "ft":"cm",
+ "yd":"m", "m":"yd",
+ "wpi":"wpcm", "wpcm":"wpi",
+ "epi":"epcm", "epcm":"epi",
+ "ppi":"ppcm", "ppcm":"ppi"}
+metric_units = ["m", "cm", "wpcm", "epcm","ppcm"]
+
+# 3epcm (7.5epi), 3.5-4 epcm (9-10 ppi), 1 epcm (2.5ppi)
+
+def parse_units(measure):
+ """
+ 12yd, 3ft, 2m, 20cm, 200mm, 10in
+ 7epi, 7ppi, 10epcm
+ - just extract number and units
+ """
+ # Pyodide
+ match = re.match(r"(^[0-9]*\.?[0-9]*)(.*)", str(measure))
+ #p = re.compile(r"(^[0-9]*\.?[0-9]*)(.*)")
+ #found = p.findall(measure)
+ #if found:
+ # num, unit = found[0]
+ # Micropython
+ #match = re.match("(^[0-9]*\.?[0-9]*)(.*)", str(measure))
+ if match:
+ num, unit = match.group(1), match.group(2)
+ #
+ if num and not num[-1].isalpha():
+ number = float(num)
+ else:
+ number = -1
+ return number, unit
+
+
+def parse_to_US(measure):
+ """
+ 12yd, 3ft, 2m, 20cm, 200mm, 10in
+ 7epi, 7ppi, 10epcm
+ - convert to yd or epi
+ """
+ number, unit = parse_units(measure)
+ if unit in convert_factors.keys():
+ val = number * convert_factors[unit]
+ if val == int(val):
+ val = int(val)
+ else:
+ #print("Sorry - did not understand units:",measure)
+ #val, unit = -1, None
+ val, unit = int(number), "yd" # more useful for this application
+ return val, unit
+
+def convert_sett(sett):
+ """
+ if epi then return inches conversion factor
+ else cm factor
+ """
+ if sett in ['epi', 'wpi', 'ppi']:
+ return convert_factors['in']
+ else:
+ return convert_factors['cm']
+
+def convert_imp(value, units, inches=False):
+ """
+ Convert value from yards into units.
+ - if inches then from inches to units
+ Return a well presented float. 0,1 decimal place
+ """
+ result = value / convert_factors[units]
+ if inches:
+ result *= convert_factors['in']
+ remainder = (result - int(result))
+ # small values get meaningful decimal points of accuracy
+ if remainder >= result/100:
+ return f"{result:.1f}{units}"
+ else:
+ return f"{result:.0f}{units}"
+
+def format_nice(value):
+ """
+ use single decimal point
+ - remove '.0' for neatness
+ """
+ vstr = f"{value:1.1f}"
+ if vstr[-2:] == ".0":
+ vstr = vstr[:-2]
+ return vstr
+
+
+
+# Testing
+if __name__ == "__main__":
+ a = "22m"
+ p = parse_units(a)
+ print(f"Parse: '{a}' = {p} or nicely as '{format_nice(p[0])}'")
+ imp = parse_to_US(a)
+ print(f"in yds (imperial) = {imp[0]}")
+ m = convert_imp(imp[0], "cm")
+ print(f"and back to '{m}'")
+ print(f"or perhaps {imp[0] / convert_factors['ft']:1.2f} 'ft'")
+
+
+
\ No newline at end of file
diff --git a/ltk 04 -project starter/index.html b/ltk 04 -project starter/index.html
new file mode 100644
index 0000000..774a702
--- /dev/null
+++ b/ltk 04 -project starter/index.html
@@ -0,0 +1,101 @@
+
+
+
+ Newtitle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ltk 04 -project starter/main.css b/ltk 04 -project starter/main.css
new file mode 100644
index 0000000..0fef763
--- /dev/null
+++ b/ltk 04 -project starter/main.css
@@ -0,0 +1,56 @@
+.description {
+ font-size: 1.0rem;
+ font-style: light;
+ border: 2px dashed #e232ca;
+}
+.basetext {
+ font-size: 1.4rem;
+ padding: 3px;
+}
+.short {
+ width: 60px;
+ align-self: center;
+ padding: 3px;
+}
+#graphic {
+ border: 1px dashed #0d720d;
+}
+
+/* from Dimension_component's css file */
+.param {
+ border: 3px dashed #1999b3;
+}
+.paramtitle {
+ font-family: "Inter", sans-serif;
+ font-size: 1.4rem;
+ font-style: normal;
+ margin: 0px 0px 0px 8px;
+}
+.measure {
+ font-family: "Inter", sans-serif;
+ font-size: 1rem;
+ height: 2.5rem;
+ width: 90px;
+ margin: 4px;
+ padding: 8px;
+ background-color: #fefdeb;
+ border-radius: 5px;
+ border: 0.5px solid black;
+}
+.short {
+ width: 60px;
+ background-color: #e2fde4;
+}
+.altlabel {
+ font-size: 0.9rem;
+}
+.noborder {
+ border: none;
+ background-color: #ffffff;
+}
+
+/* ltk overrides */
+.ltk-input {
+ font-size: 1.4rem;
+ padding: 3px;
+}
\ No newline at end of file
diff --git a/ltk 04 -project starter/main.py b/ltk 04 -project starter/main.py
new file mode 100644
index 0000000..2942d41
--- /dev/null
+++ b/ltk 04 -project starter/main.py
@@ -0,0 +1,169 @@
+#imports
+import ltk
+import svg as SVG
+import random
+import dimension_component as DC
+
+from pyscript import config
+MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython
+
+if '__terminal__' in locals(): # resize terminal if started in index.html
+ __terminal__.resize(60, 10)
+
+# globals
+g_widget = None # Hold ref to widget for nested components
+# for SVG
+solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"}
+
+def create_svg_box(size=5):
+ overall = 100
+ box_size = 100 / size
+ highlight_svg = SVG.svg(width=overall, height=overall,
+ preserveAspectRatio="xMidYMid meet",
+ viewBox=f"0 0 {overall+2} {overall+2}")
+ # Transform the group to move its children
+ grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style)
+ highlight_svg.appendChild(grp)
+ grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size))
+ #
+ return highlight_svg
+
+## Events
+# - moved event to Reactive area for event control so no global handlers
+# UNUSED
+def button_click_handler_dperecated(event):
+ print("The non-reactive button has been clicked, the svg transformed")
+ print(f"- checkbox clicked {g_widget.params.count_check_ticks} times.")
+ # move the svg child
+ x,y = random.randrange(0, 100-20), random.randrange(0, 100-20)
+ ltk.find("#highlight").attr("transform", f"translate({x} {y})")
+
+### Reactive UI variables and behaviour
+class Reactive_params(ltk.Model):
+ """
+ """
+ # These variables are all reactive
+ check_me = False
+ Drawin = 12 # Can use Drawin: int = 12 but python runtime does not check
+ Draw_label = "Subtract 2"
+
+ def __init__(self):
+ """
+ variables instanced here will not be reactive (only the classvars above)
+ but its a good place to put all the other variables your widget needs.
+ """
+ super().__init__()
+ self.inhibit_update = False
+ self.count_check_ticks = 0 # as a useless example
+
+ def changed(self, name, value):
+ """
+ Called whenever a UI element is changed by user.
+ - name is always a string.
+ Do whatever changes are required in here
+ Also invoked for internal changes
+ - inhibit_update is used to prevent needless rippling
+ """
+ #print("- Change requested:",name,value)
+ if not self.inhibit_update:
+ self.inhibit_update = True # inhibit rippling calls while we update
+ print("Changing:", name, value)
+ if name == "check_me":
+ # changing another reactive value does not ripple because of global inhibit var
+ if self.check_me:
+ self.Draw_label = "Add 2 extra"
+ self.Drawin -= 2
+ else:
+ self.Draw_label = "Subtract 2"
+ self.Drawin += 2
+ # reset ripple updates
+ self.inhibit_update = False
+
+ def button_click_handler(self, event):
+ print("The non-reactive button has been clicked, the svg transformed")
+ print(f"- checkbox clicked {self.count_check_ticks} times.")
+ # move the svg child
+ x,y = random.randrange(0, 100-20), random.randrange(0, 100-20)
+ ltk.find("#highlight").attr("transform", f"translate({x} {y})")
+
+### UI
+class UI_widget(object):
+ """
+ Top level object that shows App.
+ """
+
+ def __init__(self):
+ self.params = Reactive_params()
+ self.explanation = ["Ltk project example:",
+ "- Everything from starter_03 and building a UI element from a separate component file.",
+ "- This means you can construct standalone UI components to include when needed.",
+ "- Making single file components that can be reused is critical for large projects.",
+ "",
+ " In the index.html - you can uncomment script lines to run:",
+ "- the base module that is imported 'util_units.y' which runs a test",
+ "- the single file component (with tests) 'dimension_component.py'",
+ "- or the main.py which uses the single file component twice",
+ "",
+ "(Use ctrl-shift while moving the mouse over ltk UI elements for descriptions.)"
+ ]
+
+ def label_box(self):
+ """
+ Can make parts of the UI like this
+ """
+ return (ltk.HBox(
+ ltk.Label(self.params.Draw_label),
+ ltk.Checkbox(self.params.check_me).attr("id", "check_1")
+ ).css("border","1px dashed cyan")
+ )
+
+ def create(self):
+ """
+ Return the UI structure
+ """
+ # Use a single file component defined elsewhere
+ measure1 = DC.Component("Length", defaults=["250","yd","m"], inch_base=False).create()
+ measure2 = DC.Component("Width", defaults=["22","yd","m"], inch_base=False).create()
+ return (
+ ltk.VBox(
+ ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way
+ ltk.Div( # put your UI in here
+ # - can easily just remove these dummy panes when you no longer need the terminal
+ ltk.VBox(
+ ltk.Button("Click me!", self.params.button_click_handler).attr("id","my-button"),
+ self.label_box(),
+ ltk.HBox(
+ ltk.Label("Draw-in").addClass("basetext"),
+ ltk.Input(self.params.Drawin).attr("type","number").attr("id","drawin").addClass("short")
+ ),
+ measure1,
+ measure2
+ ),
+ ltk.Div().attr("id", "graphic"),
+ # our description
+ ltk.Paragraph(" ".join(self.explanation)).attr("id","explanation").addClass("description")
+ ).attr("id", "mydiv").css("border", "2px solid red"),
+ # Other side for the terminal
+ ltk.Div().attr("id", "forterm").css("border", "2px solid green"),
+ "Temp-split-pane"
+ )
+ )
+ )
+
+
+if __name__ == "__main__":
+ g_widget = UI_widget()
+ widget = g_widget.create()
+ widget.appendTo(ltk.window.document.body) # ui is now running
+ #
+ # move terminal to sliding pane
+ term = ltk.find("py-terminal")[0]
+ term_box = ltk.find("#forterm")[0]
+ term_box.appendChild(term)
+ #
+ print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})")
+ # svg insert into Div
+ somesvg = create_svg_box()
+ ltk.find("#graphic").html(somesvg.outerHTML)
+
+
\ No newline at end of file
diff --git a/ltk 04 -project starter/pyscript_mpy.toml b/ltk 04 -project starter/pyscript_mpy.toml
new file mode 100644
index 0000000..7323fb7
--- /dev/null
+++ b/ltk 04 -project starter/pyscript_mpy.toml
@@ -0,0 +1,15 @@
+name = "Newtitle"
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+#local filesystem
+"./components/util_units.py" = "./util_units.py"
+"./components/dimension_component.py" = "./dimension_component.py"
+"./svg.py" = "./svg.py"
\ No newline at end of file
diff --git a/ltk 04 -project starter/pyscript_py.toml b/ltk 04 -project starter/pyscript_py.toml
new file mode 100644
index 0000000..e3f34d8
--- /dev/null
+++ b/ltk 04 -project starter/pyscript_py.toml
@@ -0,0 +1,12 @@
+name = "Newtitle"
+
+packages = [ "pyscript-ltk==0.2.30"]
+
+[files]
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js"
+"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css"
+
+#local filesystem
+"./components/util_units.py" = "./util_units.py"
+"./components/dimension_component.py" = "./dimension_component.py"
+"./svg.py" = "./svg.py"
diff --git a/ltk 04 -project starter/svg.py b/ltk 04 -project starter/svg.py
new file mode 100644
index 0000000..05c94ed
--- /dev/null
+++ b/ltk 04 -project starter/svg.py
@@ -0,0 +1,179 @@
+"""
+A rewrite of Brython's SVG module, to remove JavaScript / document related
+interactions (so this can be used within a web worker, where document is not
+conveniently available).
+
+Author: Nicholas H.Tollervey (ntollervey@anaconda.com)
+Based on original work by: Romain Casati
+Mods for class,todom from Neon22(https://github.com/Neon22)
+
+License: GPL v3 or higher.
+"""
+from pyscript import document
+
+def todom(node):
+ """
+ Get back entity we can jam into the dom live.
+ - If your inserted svg is being ignored then use this as a wrapper before inserting
+ """
+ svgnode = document.createElementNS('http://www.w3.org/2000/svg', node.tagName)
+ for a in node.attributes:
+ svgnode.setAttributeNS(None,a, node.attributes[a])
+ return svgnode
+
+
+class Node:
+ """
+ Represents a node in the DOM.
+ """
+
+ def __init__(self, **kwargs):
+ self._node = kwargs
+ self.parent = kwargs.get("parent")
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML.
+ """
+ return NotImplemented
+
+
+class ElementNode(Node):
+ """
+ An element defined by a tag, may have attributes and children.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.tagName = kwargs["tagName"]
+ self.attributes = kwargs.get("attributes", {})
+ self.value = kwargs.get("value")
+ self.childNodes = []
+
+ def appendChild(self, child):
+ """
+ Add a child node to the children of this node. Using DOM API naming
+ conventions.
+ """
+ child.parent = self
+ self.childNodes.append(child)
+
+ def setAttribute(self, key, value):
+ """
+ Sets an attribute on the node.
+ """
+ self.attributes[key] = value
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML. Using DOM API
+ naming conventions.
+ """
+ result = "<" + self.tagName
+ for attr, val in self.attributes.items():
+ result += " " + attr + '="' + str(val) + '"'
+ result += ">"
+ result += self.innerHTML
+ result += "" + self.tagName + ">"
+ return result
+
+ @property
+ def innerHTML(self):
+ """
+ Get a string representation of the element's inner HTML. Using DOM API
+ naming conventions.
+ """
+ result = ""
+ for child in self.childNodes:
+ result += child.outerHTML
+ return result
+
+
+class TextNode(Node):
+ """
+ Textual content inside an ElementNode.
+ """
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.nodeValue = kwargs.get("nodeValue")
+
+ @property
+ def outerHTML(self):
+ """
+ Get a string representation of the element's outer HTML.
+ """
+ return self.nodeValue
+
+
+_svg_ns = "http://www.w3.org/2000/svg"
+_xlink_ns = "http://www.w3.org/1999/xlink"
+
+
+def _tag_func(tag):
+ def func(*args, **kwargs):
+ node = ElementNode(tagName=tag)
+ # this is mandatory to display svg properly
+ if tag == "svg":
+ node.setAttribute("xmlns", _svg_ns)
+ for arg in args:
+ if isinstance(arg, (str, int, float)):
+ arg = TextNode(nodeValue=str(arg))
+ node.appendChild(arg)
+ for key, value in kwargs.items():
+ key = key.lower()
+ if key[0:2] == "on":
+ # Ignore event handlers within the SVG. This shouldn't happen.
+ pass
+ elif key == "style":
+ node.setAttribute(
+ "style", "; ".join(f"{k}: {v}" for k, v in value.items())
+ )
+ elif key == "_class":
+ node.setAttribute(
+ "class", str(value))
+ elif value is not False:
+ node.setAttribute(key.replace("_", "-"), str(value))
+ return node
+
+ return func
+
+
+a = _tag_func("a")
+altGlyph = _tag_func("altGlyph")
+altGlyphDef = _tag_func("altGlyphDef")
+altGlyphItem = _tag_func("altGlyphItem")
+animate = _tag_func("animate")
+animateColor = _tag_func("animateColor")
+animateMotion = _tag_func("animateMotion")
+animateTransform = _tag_func("animateTransform")
+circle = _tag_func("circle")
+clipPath = _tag_func("clipPath")
+color_profile = _tag_func("color_profile")
+cursor = _tag_func("cursor")
+defs = _tag_func("defs")
+desc = _tag_func("desc")
+ellipse = _tag_func("ellipse")
+feBlend = _tag_func("feBlend")
+foreignObject = _tag_func("foreignObject")
+g = _tag_func("g")
+image = _tag_func("image")
+line = _tag_func("line")
+linearGradient = _tag_func("linearGradient")
+marker = _tag_func("marker")
+mask = _tag_func("mask")
+path = _tag_func("path")
+pattern = _tag_func("pattern")
+polygon = _tag_func("polygon")
+polyline = _tag_func("polyline")
+radialGradient = _tag_func("radialGradient")
+rect = _tag_func("rect")
+set = _tag_func("set")
+stop = _tag_func("stop")
+svg = _tag_func("svg")
+text = _tag_func("text")
+tref = _tag_func("tref")
+tspan = _tag_func("tspan")
+use = _tag_func("use")
\ No newline at end of file