New Upstream Release - python-railroad-diagrams

Ready changes

Summary

Merged new upstream version: 3.0.1 (was: 2.0.4).

Resulting package

Built on 2023-05-19T14:57 (took 5m2s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-railroad-diagrams

Lintian Result

Diff

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..70e51a8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Tab Atkins Jr.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PKG-INFO b/PKG-INFO
index 9c4738d..3df634d 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,177 +1,164 @@
 Metadata-Version: 2.1
 Name: railroad-diagrams
-Version: 1.1.1
+Version: 3.0.1
 Summary: Generate SVG railroad syntax diagrams, like on JSON.org.
 Home-page: https://github.com/tabatkins/railroad-diagrams
 Author: Tab Atkins
 Author-email: jackalmage@gmail.com
 License: UNKNOWN
-Description: Railroad-Diagram Generator
-        ==========================
-        
-        <a href="https://github.com/tabatkins/railroad-diagrams/blob/gh-pages/images/rr-title.svg"><img src="https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-title.svg?sanitize=true" alt="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" title="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" width=10000></a>
-        
-        This is a small library for generating railroad diagrams
-        (like what [JSON.org](http://json.org) uses)
-        using SVG, with both JS and Python ports.
-        
-        Railroad diagrams are a way of visually representing a grammar
-        in a form that is more readable than using regular expressions or BNF.
-        They can easily represent any context-free grammar, and some more powerful grammars.
-        There are several railroad-diagram generators out there, but none of them had the visual appeal I wanted, so I wrote my own.
-        
-        [Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
-        
-        (This is the README for the Python port;
-        to see the JS README, visit <https://github.com/tabatkins/railroad-diagrams>.)
-        
-        Diagrams
-        --------
-        
-        Constructing a diagram is a set of nested calls:
-        
-        ```python
-        from railroad import Diagram, Choice
-        d = Diagram("foo", Choice(0, "bar", "baz"))
-        d.writeSvg(sys.stdout.write)
-        ```
-        
-        A railroad diagram must be started as a `Diagram` object,
-        which takes a list of diagram items,
-        defined below.
-        
-        The `Diagram()` constructor also optionally takes some keyword arguments:
-        
-        * `css`: If passed, is the CSS you would like the diagram to include.
-            If you don't pass anything, it defaults to including `railroad.DEFAULT_STYLE`.
-            If you don't want it to include any css at all in the diagram
-            (perhaps because you're including the `railroad.css` file manually in your page, and don't need each diagram to duplicate the CSS in itself),
-            pass `css=None`.
-        * `type`: JSON.org, the inspiration for these diagram's styling, technically has two varieties of Diagrams: a "simple" kind it uses for "leaf" types like numbers, and a "complex" kind which is used for container types like arrays. The only difference is the shape of the start/end indicators of the diagram.
-        
-            Diagrams default to being "simple", but you can manually choose by passing `type="simple"` or `type="complex"`.
-        
-        After constructing a Diagram, you can call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
-        
-        To output the diagram, call `.writeSvg(cb)` on it, passing a function that'll get called repeatedly to produce the SVG markup. `sys.stdout.write` (or the `.write` property of any file object) is a great value to pass if you're directly outputting it; if you need it as a plain string, a `StringIO` can be used.
-        
-        If you need to walk the component tree of a diagram for some reason, `Diagram` has a `.walk(cb)` method as well, which will call your callback on every node in the diagram, in a "pre-order depth-first traversal" (the node first, then each child).
-        
-        Components
-        ----------
-        
-        Components are either leaves (containing only text or similar)
-        or containers (containing other components).
-        
-        The leaves:
-        * Terminal(text, href?, title?, cls?) or a bare string - represents literal text.
-        
-            All arguments past the first are optional:
-            * 'href' makes the text a hyperlink with the given URL
-            * 'title' adds an SVG `<title>` element to the element,
-                giving it "hover text"
-                and a description for screen-readers and other assistive tech
-            * 'cls' is additional classes to apply to the element,
-                beyond the default `'terminal'`
-        
-        * NonTerminal(text, href) - represents an instruction or another production.
-        
-            The optional arguments have the same meaning as for Terminal,
-            except that the default class is `'non-terminal'`.
-        
-        * Comment(text, href) - a comment.
-        
-            The optional arguments have the same meaning as for Terminal,
-            except that the default class is `'non-terminal'`.
-        
-        * Skip() - an empty line
-        
-        * Start(type, label) and End(type) - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a Start() explicitly (as the first child of the Diagram!). The "type" attribute takes either "simple" (the default) or "complex", a la Diagram() and ComplexDiagram(). All arguments are optional.
-        
-        The containers:
-        * Sequence(...children) - like simple concatenation in a regex.
-        
-            ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
-        
-        * Stack(...children) - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
-        
-            ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
-        
-        * OptionalSequence(...children) - a Sequence where every item is *individually* optional, but at least one item must be chosen
-        
-            ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
-        
-        * Choice(index, ...children) - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle (starting from 0 for the first child).
-        
-            ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
-        
-        * MultipleChoice(index, type, ...children) - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either "any" (1+ branches can be taken) or "all" (all branches must be taken).
-        
-            ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
-        
-        * HorizontalChoice(...children) - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
-        
-        	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
-        
-        * Optional(child, skip?) - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter is `True`, it instead puts the Skip() in the straight-line path, for when the "normal" behavior is to omit the item.
-        
-            ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
-        
-        * OneOrMore(child, repeat?) - like `+` in a regex.  The 'repeat' argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
-        
-            ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
-        
-        * AlternatingSequence(option1, option2) - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
-        
-            ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
-        
-        * ZeroOrMore(child, repeat?, skip?) - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
-        
-            ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
-        
-        * Group(child, label?) - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
-        
-            ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
-        
-        
-        Options
-        -------
-        
-        There are a few options you can tweak, living as UPPERCASE_CONSTANTS at the top of the module; these can be adjusted via `railroad.OPTION_NAME_HERE = "whatever"`.
-        Note that if you change the text sizes in the CSS,
-        you'll have to adjust the text metrics here as well.
-        
-        * VS - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
-        * AR - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
-        * DIAGRAM_CLASS - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
-        * STROKE_ODD_PIXEL_LENGTH - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
-        * INTERNAL_ALIGNMENT - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
-        * CHAR_WIDTH - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
-        * COMMENT_CHAR_WIDTH - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
-        * DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
-        
-        Caveats
-        -------
-        
-        SVG can't actually respond to the sizes of content; in particular, there's no way to make SVG adjust sizing/positioning based on the length of some text.  Instead, I guess at some font metrics, which mostly work as long as you're using a fairly standard monospace font.  This works pretty well, but long text inside of a construct might eventually overflow the construct.
-        
-        License
-        -------
-        
-        This document and all associated files in the github project are licensed under [CC0](http://creativecommons.org/publicdomain/zero/1.0/) ![](http://i.creativecommons.org/p/zero/1.0/80x15.png).
-        This means you can reuse, remix, or otherwise appropriate this project for your own use **without restriction**.
-        (The actual legal meaning can be found at the above link.)
-        Don't ask me for permission to use any part of this project, **just use it**.
-        I would appreciate attribution, but that is not required by the license.
-        
 Keywords: diagrams,syntax,grammar,railroad diagrams
 Platform: UNKNOWN
-Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
+Classifier: License :: OSI Approved :: MIT License
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Requires-Python: >=3.7
 Description-Content-Type: text/markdown
+License-File: LICENSE
+
+Railroad-Diagram Generator, Python Version
+==========================================
+
+This is a small library for generating railroad diagrams
+(like what [JSON.org](http://json.org) uses)
+using SVG, with both JS and Python ports.
+[Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
+
+(This is the README for the Python port;
+see the [main README](https://github.com/tabatkins/railroad-diagrams) for other ports,
+and for more non-Python-specific information.)
+
+Diagrams
+--------
+
+Constructing a diagram is a set of nested calls:
+
+```python
+from railroad import Diagram, Choice
+d = Diagram("foo", Choice(0, "bar", "baz"))
+d.writeSvg(sys.stdout.write)
+```
+
+A railroad diagram must be started as a `Diagram` object,
+which takes a list of diagram items,
+defined below.
+
+The `Diagram()` constructor also optionally takes some keyword arguments:
+
+* `css`: If passed, is the CSS you would like the diagram to include.
+    If you don't pass anything, it defaults to including `railroad.DEFAULT_STYLE`.
+    If you don't want it to include any css at all in the diagram
+    (perhaps because you're including the `railroad.css` file manually in your page, and don't need each diagram to duplicate the CSS in itself),
+    pass `css=None`.
+* `type`: JSON.org, the inspiration for these diagram's styling, technically has two varieties of Diagrams: a "simple" kind it uses for "leaf" types like numbers, and a "complex" kind which is used for container types like arrays. The only difference is the shape of the start/end indicators of the diagram.
+
+    Diagrams default to being "simple", but you can manually choose by passing `type="simple"` or `type="complex"`.
+
+After constructing a Diagram, you can call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
+
+To output the diagram, call `.writeSvg(cb)` on it, passing a function that'll get called repeatedly to produce the SVG markup. `sys.stdout.write` (or the `.write` property of any file object) is a great value to pass if you're directly outputting it; if you need it as a plain string, a `StringIO` can be used.
+This method produces an SVG fragment appropriate to include directly in HTML.
+
+Alternately, you can call `.writeStandalone(cb, css?)`,
+which'll format the SVG as a standalone document
+rather than as an HTML fragment.
+If you don't pass any `css`,
+it'll automatically include the `DEFAULT_STYLE`;
+you can include your own CSS instead by passing it as a string
+(or an empty string to include no CSS at all).
+
+If you need to walk the component tree of a diagram for some reason, `Diagram` has a `.walk(cb)` method as well, which will call your callback on every node in the diagram, in a "pre-order depth-first traversal" (the node first, then each child).
+
+Components
+----------
+
+Components are either leaves (containing only text or similar)
+or containers (containing other components).
+
+The leaves:
+* Terminal(text, href?, title?, cls?) or a bare string - represents literal text.
+
+    All arguments past the first are optional:
+    * 'href' makes the text a hyperlink with the given URL
+    * 'title' adds an SVG `<title>` element to the element,
+        giving it "hover text"
+        and a description for screen-readers and other assistive tech
+    * 'cls' is additional classes to apply to the element,
+        beyond the default `'terminal'`
+
+* NonTerminal(text, href) - represents an instruction or another production.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'non-terminal'`.
+
+* Comment(text, href) - a comment.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'non-terminal'`.
+
+* Skip() - an empty line
+
+* Start(type, label) and End(type) - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a Start() explicitly (as the first child of the Diagram!). The "type" attribute takes either "simple" (the default) or "complex", a la Diagram() and ComplexDiagram(). All arguments are optional.
+
+The containers:
+* Sequence(...children) - like simple concatenation in a regex.
+
+    ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
+
+* Stack(...children) - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
+
+    ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
+
+* OptionalSequence(...children) - a Sequence where every item is *individually* optional, but at least one item must be chosen
+
+    ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
+
+* Choice(index, ...children) - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle (starting from 0 for the first child).
+
+    ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
+
+* MultipleChoice(index, type, ...children) - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either "any" (1+ branches can be taken) or "all" (all branches must be taken).
+
+    ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
+
+* HorizontalChoice(...children) - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
+
+	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
+
+* Optional(child, skip?) - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter is `True`, it instead puts the Skip() in the straight-line path, for when the "normal" behavior is to omit the item.
+
+    ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
+
+* OneOrMore(child, repeat?) - like `+` in a regex.  The 'repeat' argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
+
+    ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
+
+* AlternatingSequence(option1, option2) - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
+
+    ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
+
+* ZeroOrMore(child, repeat?, skip?) - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
+
+    ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
+
+* Group(child, label?) - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
+
+    ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
+
+
+Options
+-------
+
+There are a few options you can tweak, living as UPPERCASE_CONSTANTS at the top of the module; these can be adjusted via `railroad.OPTION_NAME_HERE = "whatever"`.
+Note that if you change the text sizes in the CSS,
+you'll have to adjust the text metrics here as well.
+
+* VS - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
+* AR - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
+* DIAGRAM_CLASS - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
+* STROKE_ODD_PIXEL_LENGTH - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
+* INTERNAL_ALIGNMENT - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
+* CHAR_WIDTH - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
+* COMMENT_CHAR_WIDTH - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
+* DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
+
diff --git a/README-js.md b/README-js.md
new file mode 100644
index 0000000..887a961
--- /dev/null
+++ b/README-js.md
@@ -0,0 +1,150 @@
+Railroad-Diagram Generator, JS Version
+=======================================
+
+This is a small library for generating railroad diagrams
+(like what [JSON.org](http://json.org) uses)
+using SVG, with both JS and Python ports.
+[Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
+
+(This is the README for the JS port;
+see the [main README](https://github.com/tabatkins/railroad-diagrams) for other ports, 
+and for more non-JS-specific information.)
+
+Diagrams
+--------
+
+To use the library,
+include `railroad.css` in your page,
+and import the `railroad.js` module in your script,
+then call the Diagram() function.
+Its arguments are the components of the diagram
+(Diagram is a special form of Sequence).
+
+The constructors for each node are named exports in the module;
+the default export is an object of same-named functions that just call the constructors,
+so you can construct diagrams without having to spam `new` all over the place:
+
+```js
+// Use the constructors
+import {Diagram, Choice} from "./railroad.js";
+const d = new Diagram("foo", new Choice(0, "bar", "baz"));
+
+// Or use the functions that call the constructors for you
+import rr from "./railroad.js";
+const d = rr.Diagram("foo", rr.Choice(0, "bar", "baz"));
+```
+
+Alternately, you can call ComplexDiagram();
+it's identical to Diagram(),
+but has slightly different start/end shapes,
+same as what JSON.org does to distinguish between "leaf" types like number (ordinary Diagram())
+and "container" types like Array (ComplexDiagram()).
+
+The Diagram class also has a few methods:
+
+* `.walk(cb)` calls the cb function on the diagram, then on its child nodes, recursing down the tree. This is a "pre-order depth-first" traversal, if you're into that sort of thing - the first child's children are visited before the diagram's second child. (In other words, the same order you encounter their constructors in the code that created the diagram.) Use this if you want to, say, sanitize things in the diagram.
+* `.format(...paddings)` "formats" the Diagram to make it ready for output. Pass it 0-4 paddings, interpreted just like the CSS `padding` property, to give it some "breathing room" around its box; these default to `20` if not specified. This is automatically called by the output functions if you don't do so yourself, so if the default paddings suffice, there's no need to worry about this.
+* `.toString()` outputs the SVG of the diagram as a string, ready to be put into your HTML. This is *not* a standalone SVG file; it's intended to be embedded into HTML.
+* `.toStandalone()` outputs the SVG of the diagram as a string, but this *is* a standalone SVG file.
+* `.toSVG()` outputs the diagram as an actual `<svg>` DOM element, ready for appending into a document.
+* `.addTo(parent?)` directly appends the diagram, as an `<svg>` element, to the specified parent element. If you omit the parent element, it instead appends to the script element it's being called from, so you can easily insert a diagram into your document by just dropping a tiny inline `<script>` that just calls `new Diagram(...).addTo()` where you want the diagram to show up.
+
+
+Components
+----------
+
+Components are either leaves or containers.
+
+The leaves:
+* `Terminal(text[, {href, title, cls}])` or a bare string - represents literal text.
+
+    All the properties in the options bag are optional:
+    * `href` makes the text a hyperlink with the given URL
+    * `title` adds an SVG `<title>` element to the element,
+        giving it "hover text"
+        and a description for screen-readers and other assistive tech
+    * `cls` is additional classes to apply to the element,
+        beyond the default `'terminal'`
+
+* `NonTerminal(text[, {href, title, cls}])` - represents an instruction or another production.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'non-terminal'`.
+
+* `Comment(text[, {href, title, cls}])` - a comment.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'comment'`.
+
+* `Skip()` - an empty line
+
+* `Start({type, label})` and `End({type})` - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a `Start()` explicitly (as the first child of the Diagram!). The `type` property takes either `"simple"` (the default) or `"complex"`, a la `Diagram()` and `ComplexDiagram()`. All properties are optional.
+
+The containers:
+* `Sequence(...children)` - like simple concatenation in a regex.
+
+    ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
+
+* `Stack(...children)` - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
+
+    ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
+
+* `OptionalSequence(...children)` - a Sequence where every item is *individually* optional, but at least one item must be chosen
+
+    ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
+
+* `Choice(index, ...children)` - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle
+
+    ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
+
+* `MultipleChoice(index, type, ...children)` - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either `"any"` (1+ branches can be taken) or `"all"` (all branches must be taken).
+
+    ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
+
+* `HorizontalChoice(...children)` - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
+
+	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
+
+* `Optional(child[, skip])` - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter has the value `"skip"`, it instead puts the `Skip()` in the straight-line path, for when the "normal" behavior is to omit the item.
+
+
+    ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
+
+* `OneOrMore(child[, repeat])` - like `+` in a regex.  The `repeat` argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
+
+    ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
+
+* `AlternatingSequence(option1, option2)` - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
+
+    ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
+
+* `ZeroOrMore(child[, repeat[, skip]])` - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
+
+    ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
+
+* `Group(child[, label])` - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
+
+    ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
+
+After constructing a Diagram, call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
+
+The result can either be `.toString()`'d for the markup, or `.toSVG()`'d for an `<svg>` element, which can then be immediately inserted to the document.  As a convenience, Diagram also has an `.addTo(element)` method, which immediately converts it to SVG and appends it to the referenced element with default paddings. `element` defaults to `document.body`.
+
+Options
+-------
+
+There are a few options you can tweak,
+in an `Options` object exported from the module.
+Just tweak either until the diagram looks like what you want.
+You can also change the CSS file - feel free to tweak to your heart's content.
+Note, though, that if you change the text sizes in the CSS,
+you'll have to go adjust the options specifying the text metrics as well.
+
+* `Options.VS` - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
+* `Options.AR` - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
+* `Options.DIAGRAM_CLASS` - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
+* `Options.STROKE_ODD_PIXEL_LENGTH` - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
+* `Options.INTERNAL_ALIGNMENT` - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
+* `Options.CHAR_WIDTH` - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
+* `Options.COMMENT_CHAR_WIDTH` - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
+* `Options.DEBUG` - if `true`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `false`.
diff --git a/README-py.md b/README-py.md
index cb3869b..a630e43 100644
--- a/README-py.md
+++ b/README-py.md
@@ -1,21 +1,14 @@
-Railroad-Diagram Generator
-==========================
-
-<a href="https://github.com/tabatkins/railroad-diagrams/blob/gh-pages/images/rr-title.svg"><img src="https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-title.svg?sanitize=true" alt="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" title="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" width=10000></a>
+Railroad-Diagram Generator, Python Version
+==========================================
 
 This is a small library for generating railroad diagrams
 (like what [JSON.org](http://json.org) uses)
 using SVG, with both JS and Python ports.
-
-Railroad diagrams are a way of visually representing a grammar
-in a form that is more readable than using regular expressions or BNF.
-They can easily represent any context-free grammar, and some more powerful grammars.
-There are several railroad-diagram generators out there, but none of them had the visual appeal I wanted, so I wrote my own.
-
 [Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
 
 (This is the README for the Python port;
-to see the JS README, visit <https://github.com/tabatkins/railroad-diagrams>.)
+see the [main README](https://github.com/tabatkins/railroad-diagrams) for other ports,
+and for more non-Python-specific information.)
 
 Diagrams
 --------
@@ -46,6 +39,15 @@ The `Diagram()` constructor also optionally takes some keyword arguments:
 After constructing a Diagram, you can call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
 
 To output the diagram, call `.writeSvg(cb)` on it, passing a function that'll get called repeatedly to produce the SVG markup. `sys.stdout.write` (or the `.write` property of any file object) is a great value to pass if you're directly outputting it; if you need it as a plain string, a `StringIO` can be used.
+This method produces an SVG fragment appropriate to include directly in HTML.
+
+Alternately, you can call `.writeStandalone(cb, css?)`,
+which'll format the SVG as a standalone document
+rather than as an HTML fragment.
+If you don't pass any `css`,
+it'll automatically include the `DEFAULT_STYLE`;
+you can include your own CSS instead by passing it as a string
+(or an empty string to include no CSS at all).
 
 If you need to walk the component tree of a diagram for some reason, `Diagram` has a `.walk(cb)` method as well, which will call your callback on every node in the diagram, in a "pre-order depth-first traversal" (the node first, then each child).
 
@@ -140,18 +142,4 @@ you'll have to adjust the text metrics here as well.
 * INTERNAL_ALIGNMENT - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
 * CHAR_WIDTH - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
 * COMMENT_CHAR_WIDTH - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
-* DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
-
-Caveats
--------
-
-SVG can't actually respond to the sizes of content; in particular, there's no way to make SVG adjust sizing/positioning based on the length of some text.  Instead, I guess at some font metrics, which mostly work as long as you're using a fairly standard monospace font.  This works pretty well, but long text inside of a construct might eventually overflow the construct.
-
-License
--------
-
-This document and all associated files in the github project are licensed under [CC0](http://creativecommons.org/publicdomain/zero/1.0/) ![](http://i.creativecommons.org/p/zero/1.0/80x15.png).
-This means you can reuse, remix, or otherwise appropriate this project for your own use **without restriction**.
-(The actual legal meaning can be found at the above link.)
-Don't ask me for permission to use any part of this project, **just use it**.
-I would appreciate attribution, but that is not required by the license.
+* DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
\ No newline at end of file
diff --git a/README.md b/README.md
index 47df6b9..9c7246a 100644
--- a/README.md
+++ b/README.md
@@ -14,146 +14,16 @@ There are several railroad-diagram generators out there, but none of them had th
 
 [Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
 
-(For Python, see [the Python README](https://github.com/tabatkins/railroad-diagrams/blob/gh-pages/README-py.md), or just `pip install railroad-diagrams`.)
-
-Diagrams
+Versions
 --------
 
-To use the library,
-include `railroad.css` in your page,
-and import the `railroad.js` module in your script,
-then call the Diagram() function.
-Its arguments are the components of the diagram
-(Diagram is a special form of Sequence).
-
-The constructors for each node are named exports in the module;
-the default export is an object of same-named functions that just call the constructors,
-so you can construct diagrams without having to spam `new` all over the place:
-
-```js
-// Use the constructors
-import {Diagram, Choice} from "./railroad.js";
-const d = new Diagram("foo", new Choice(0, "bar", "baz"));
-
-// Or use the functions that call the constructors for you
-import rr from "./railroad.js";
-const d = rr.Diagram("foo", rr.Choice(0, "bar", "baz"));
-```
-
-Alternately, you can call ComplexDiagram();
-it's identical to Diagram(),
-but has slightly different start/end shapes,
-same as what JSON.org does to distinguish between "leaf" types like number (ordinary Diagram())
-and "container" types like Array (ComplexDiagram()).
-
-The Diagram class also has a few methods:
-
-* `.walk(cb)` calls the cb function on the diagram, then on its child nodes, recursing down the tree. This is a "pre-order depth-first" traversal, if you're into that sort of thing - the first child's children are visited before the diagram's second child. (In other words, the same order you encounter their constructors in the code that created the diagram.) Use this if you want to, say, sanitize things in the diagram.
-* `.format(...paddings)` "formats" the Diagram to make it ready for output. Pass it 0-4 paddings, interpreted just like the CSS `padding` property, to give it some "breathing room" around its box; these default to `20` if not specified. This is automatically called by the output functions if you don't do so yourself, so if the default paddings suffice, there's no need to worry about this.
-* `.toString()` outputs the SVG of the diagram as a string, ready to be put into your HTML. This is *not* a standalone SVG file; it's intended to be embedded into HTML.
-* `.toStandalone()` outputs the SVG of the diagram as a string, but this *is* a standalone SVG file.
-* `.toSVG()` outputs the diagram as an actual `<svg>` DOM element, ready for appending into a document.
-* `.addTo(parent?)` directly appends the diagram, as an `<svg>` element, to the specified parent element. If you omit the parent element, it instead appends to the script element it's being called from, so you can easily insert a diagram into your document by just dropping a tiny inline `<script>` that just calls `new Diagram(...).addTo()` where you want the diagram to show up.
-
-
-Components
-----------
-
-Components are either leaves or containers.
-
-The leaves:
-* Terminal(text, {href, title, cls}) or a bare string - represents literal text.
-
-    All the arguments in the options bag are optional:
-    * 'href' makes the text a hyperlink with the given URL
-    * 'title' adds an SVG `<title>` element to the element,
-        giving it "hover text"
-        and a description for screen-readers and other assistive tech
-    * 'cls' is additional classes to apply to the element,
-        beyond the default `'terminal'`
-
-* NonTerminal(text, {href, title, cls}) - represents an instruction or another production.
-
-    The optional arguments have the same meaning as for Terminal,
-    except that the default class is `'non-terminal'`.
-
-* Comment(text, {href, title, cls}) - a comment.
-
-    The optional arguments have the same meaning as for Terminal,
-    except that the default class is `'comment'`.
-
-* Skip() - an empty line
-
-* Start(type, label) and End(type) - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a Start() explicitly (as the first child of the Diagram!). The "type" attribute takes either "simple" (the default) or "complex", a la Diagram() and ComplexDiagram(). All arguments are optional.
-
-The containers:
-* Sequence(...children) - like simple concatenation in a regex.
-
-    ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
-
-* Stack(...children) - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
-
-    ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
-
-* OptionalSequence(...children) - a Sequence where every item is *individually* optional, but at least one item must be chosen
+This library is supported both as a JS module and a Python module,
+and the install and use instructions for each
+are in their lang-specific READMEs.
 
-    ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
+* [JS-specific README](README-js.md)
+* [Python-specific README](README-py.md)
 
-* Choice(index, ...children) - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle
-
-    ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
-
-* MultipleChoice(index, type, ...children) - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either "any" (1+ branches can be taken) or "all" (all branches must be taken).
-
-    ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
-
-* HorizontalChoice(...children) - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
-
-	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
-
-* Optional(child, skip) - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter has the value `"skip"`, it instead puts the Skip() in the straight-line path, for when the "normal" behavior is to omit the item.
-
-
-    ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
-
-* OneOrMore(child, repeat) - like `+` in a regex.  The 'repeat' argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
-
-    ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
-
-* AlternatingSequence(option1, option2) - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
-
-    ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
-
-* ZeroOrMore(child, repeat, skip) - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
-
-    ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
-
-* Group(child, label?) - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
-
-    ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
-
-After constructing a Diagram, call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
-
-The result can either be `.toString()`'d for the markup, or `.toSVG()`'d for an `<svg>` element, which can then be immediately inserted to the document.  As a convenience, Diagram also has an `.addTo(element)` method, which immediately converts it to SVG and appends it to the referenced element with default paddings. `element` defaults to `document.body`.
-
-Options
--------
-
-There are a few options you can tweak,
-in an `Options` object exported from the module.
-Just tweak either until the diagram looks like what you want.
-You can also change the CSS file - feel free to tweak to your heart's content.
-Note, though, that if you change the text sizes in the CSS,
-you'll have to go adjust the options specifying the text metrics as well.
-
-* `Options.VS` - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
-* `Options.AR` - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
-* `Options.DIAGRAM_CLASS` - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
-* `Options.STROKE_ODD_PIXEL_LENGTH` - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
-* `Options.INTERNAL_ALIGNMENT` - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
-* `Options.CHAR_WIDTH` - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
-* `Options.COMMENT_CHAR_WIDTH` - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
-* `Options.DEBUG` - if `true`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `false`.
 
 Caveats
 -------
@@ -164,8 +34,8 @@ SVG can't actually respond to the sizes of content; in particular, there's no wa
 License
 -------
 
-This document and all associated files in the github project are licensed under [CC0](http://creativecommons.org/publicdomain/zero/1.0/) ![](http://i.creativecommons.org/p/zero/1.0/80x15.png).
-This means you can reuse, remix, or otherwise appropriate this project for your own use **without restriction**.
-(The actual legal meaning can be found at the above link.)
+Standard MIT license; see [LICENSE](LICENSE).
+
 Don't ask me for permission to use any part of this project, **just use it**.
 I would appreciate attribution, but that is not required by the license.
+If you're doing something cool with it, again I'd appreciate it if you let me know, but that's not required either.
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index 9102cf0..ea6088b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-railroad-diagrams (3.0.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 19 May 2023 14:52:39 -0000
+
 python-railroad-diagrams (1.1.1-2) unstable; urgency=medium
 
   * Source-only upload for testing migration.
diff --git a/railroad.py b/railroad.py
index 917ed39..33fa577 100644
--- a/railroad.py
+++ b/railroad.py
@@ -1,203 +1,260 @@
 # -*- coding: utf-8 -*-
-from __future__ import division, unicode_literals
-import sys
+from __future__ import annotations
+
 import math as Math
+import sys
 
-if sys.version_info >= (3, ):
-	unicode = str
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import (
+        Any,
+        Callable,
+        Dict,
+        Generator,
+        List,
+        Optional as Opt,
+        Sequence as Seq,
+        Tuple,
+        Type,
+        TypeVar,
+        Union,
+    )
+
+    T = TypeVar("T")
+    Node = Union[str, DiagramItem]  # pylint: disable=used-before-assignment
+    WriterF = Callable[[str], Any]
+    WalkerF = Callable[[DiagramItem], Any]  # pylint: disable=used-before-assignment
+    AttrsT = Dict[str, Any]
 
 # Display constants
-DEBUG = False # if true, writes some debug information into attributes
-VS = 8 # minimum vertical separation between things. For a 3px stroke, must be at least 4
-AR = 10 # radius of arcs
-DIAGRAM_CLASS = 'railroad-diagram' # class to put on the root <svg>
-STROKE_ODD_PIXEL_LENGTH = True # is the stroke width an odd (1px, 3px, etc) pixel length?
-INTERNAL_ALIGNMENT = 'center' # how to align items when they have extra space. left/right/center
-CHAR_WIDTH = 8.5 # width of each monospace character. play until you find the right value for your font
-COMMENT_CHAR_WIDTH = 7 # comments are in smaller text by default
-
-
-def e(text):
-	import re
-	return re.sub(r"[*_\`\[\]<&]", lambda c: "&#{0};".format(ord(c.group(0))), unicode(text))
-
-def determineGaps(outer, inner):
-	diff = outer - inner
-	if INTERNAL_ALIGNMENT == 'left':
-		return 0, diff
-	elif INTERNAL_ALIGNMENT == 'right':
-		return diff, 0
-	else:
-		return diff/2, diff/2
-
-def doubleenumerate(seq):
-	length = len(list(seq))
-	for i,item in enumerate(seq):
-		yield i, i-length, item
-
-def addDebug(el):
-	if not DEBUG:
-		return
-	el.attrs['data-x'] = "{0} w:{1} h:{2}/{3}/{4}".format(type(el).__name__, el.width, el.up, el.height, el.down)
-
-
-
-class DiagramItem(object):
-	def __init__(self, name, attrs=None, text=None):
-		self.name = name
-		# up = distance it projects above the entry line
-		# height = distance between the entry/exit lines
-		# down = distance it projects below the exit line
-		self.height = 0
-		self.attrs = attrs or {}
-		self.children = [text] if text else []
-		self.needsSpace = False
-
-	def format(self, x, y, width):
-		raise NotImplementedError  # Virtual
-
-	def addTo(self, parent):
-		parent.children.append(self)
-		return self
-
-	def writeSvg(self, write):
-		write(u'<{0}'.format(self.name))
-		for name, value in sorted(self.attrs.items()):
-			write(u' {0}="{1}"'.format(name, e(value)))
-		write(u'>')
-		if self.name in ["g", "svg"]:
-			write(u'\n')
-		for child in self.children:
-			if isinstance(child, DiagramItem):
-				child.writeSvg(write)
-			else:
-				write(e(child))
-		write(u'</{0}>'.format(self.name))
-
-	def walk(self, cb):
-		cb(self)
-
-	def __eq__(self, other):
-		return type(self) == type(other) and self.__dict__ == other.__dict__
-
-	def __ne__(self, other):
-		return not (self == other)
+DEBUG = False  # if true, writes some debug information into attributes
+VS = 8  # minimum vertical separation between things. For a 3px stroke, must be at least 4
+AR = 10  # radius of arcs
+DIAGRAM_CLASS = "railroad-diagram"  # class to put on the root <svg>
+STROKE_ODD_PIXEL_LENGTH = (
+    True  # is the stroke width an odd (1px, 3px, etc) pixel length?
+)
+INTERNAL_ALIGNMENT = (
+    "center"  # how to align items when they have extra space. left/right/center
+)
+CHAR_WIDTH = 8.5  # width of each monospace character. play until you find the right value for your font
+COMMENT_CHAR_WIDTH = 7  # comments are in smaller text by default
+
+
+def escapeAttr(val: Union[str, float]) -> str:
+    if isinstance(val, str):
+        return val.replace("&", "&amp;").replace("'", "&apos;").replace('"', "&quot;")
+    return f"{val:g}"
+
+
+def escapeHtml(val: str) -> str:
+    return escapeAttr(val).replace("<", "&lt;")
+
+
+def determineGaps(outer: float, inner: float) -> Tuple[float, float]:
+    diff = outer - inner
+    if INTERNAL_ALIGNMENT == "left":
+        return 0, diff
+    elif INTERNAL_ALIGNMENT == "right":
+        return diff, 0
+    else:
+        return diff / 2, diff / 2
+
+
+def doubleenumerate(seq: Seq[T]) -> Generator[Tuple[int, int, T], None, None]:
+    length = len(list(seq))
+    for i, item in enumerate(seq):
+        yield i, i - length, item
+
+
+def addDebug(el: DiagramItem) -> None:
+    if not DEBUG:
+        return
+    el.attrs["data-x"] = "{0} w:{1} h:{2}/{3}/{4}".format(
+        type(el).__name__, el.width, el.up, el.height, el.down
+    )
+
+
+class DiagramItem:
+    def __init__(self, name: str, attrs: Opt[AttrsT] = None, text: Opt[Node] = None):
+        self.name = name
+        # up = distance it projects above the entry line
+        self.up: float = 0
+        # height = distance between the entry/exit lines
+        self.height: float = 0
+        # down = distance it projects below the exit line
+        self.down: float = 0
+        # width = distance between the entry/exit lines horizontally
+        self.width: float = 0
+        # Whether the item is okay with being snug against another item or not
+        self.needsSpace = False
+
+        # DiagramItems pull double duty as SVG elements.
+        self.attrs: AttrsT = attrs or {}
+        # Subclasses store their meaningful children as .item or .items;
+        # .children instead stores their formatted SVG nodes.
+        self.children: List[Union[Node, Path, Style]] = [text] if text else []
+
+    def format(self, x: float, y: float, width: float) -> DiagramItem:
+        raise NotImplementedError  # Virtual
+
+    def addTo(self, parent: DiagramItem) -> DiagramItem:
+        parent.children.append(self)
+        return self
+
+    def writeSvg(self, write: WriterF) -> None:
+        write("<{0}".format(self.name))
+        for name, value in sorted(self.attrs.items()):
+            write(' {0}="{1}"'.format(name, escapeAttr(value)))
+        write(">")
+        if self.name in ["g", "svg"]:
+            write("\n")
+        for child in self.children:
+            if isinstance(child, (DiagramItem, Path, Style)):
+                child.writeSvg(write)
+            else:
+                write(escapeHtml(child))
+        write("</{0}>".format(self.name))
+
+    def walk(self, cb: WalkerF) -> None:
+        cb(self)
 
 
 class DiagramMultiContainer(DiagramItem):
-	def __init__(self, name, items, attrs=None, text=None):
-		DiagramItem.__init__(self, name, attrs, text)
-		self.items = [wrapString(item) for item in items]
-
-	def walk(self, cb):
-		cb(self)
-		for item in self.items:
-			item.walk(cb)
-
-
-class Path(DiagramItem):
-	def __init__(self, x, y):
-		self.x = x
-		self.y = y
-		DiagramItem.__init__(self, 'path', {'d': 'M%s %s' % (x, y)})
-
-	def m(self, x, y):
-		self.attrs['d'] += 'm{0} {1}'.format(x,y)
-		return self
-
-	def l(self, x, y):
-		self.attrs['d'] += 'l{0} {1}'.format(x,y)
-		return self
-
-	def h(self, val):
-		self.attrs['d'] += 'h{0}'.format(val)
-		return self
-
-	def right(self, val):
-		return self.h(max(0, val))
-
-	def left(self, val):
-		return self.h(-max(0, val))
-
-	def v(self, val):
-		self.attrs['d'] += 'v{0}'.format(val)
-		return self
-
-	def down(self, val):
-		return self.v(max(0, val))
-
-	def up(self, val):
-		return self.v(-max(0, val))
-
-	def arc_8(self, start, dir):
-		# 1/8 of a circle
-		arc = AR
-		s2 = 1/Math.sqrt(2) * arc
-		s2inv = (arc - s2)
-		path = "a {0} {0} 0 0 {1} ".format(arc, "1" if dir == 'cw' else "0")
-		sd = start+dir
-		if sd == 'ncw':
-			offset = [s2, s2inv]
-		elif sd == 'necw':
-			offset = [s2inv, s2]
-		elif sd == 'ecw':
-			offset = [-s2inv, s2]
-		elif sd == 'secw':
-			offset = [-s2, s2inv]
-		elif sd == 'scw':
-			offset = [-s2, -s2inv]
-		elif sd == 'swcw':
-			offset = [-s2inv, -s2]
-		elif sd == 'wcw':
-			offset = [s2inv, -s2]
-		elif sd == 'nwcw':
-			offset = [s2, -s2inv]
-		elif sd == 'nccw':
-			offset = [-s2, s2inv]
-		elif sd == 'nwccw':
-			offset = [-s2inv, s2]
-		elif sd == 'wccw':
-			offset = [s2inv, s2]
-		elif sd == 'swccw':
-			offset = [s2, s2inv]
-		elif sd == 'sccw':
-			offset = [s2, -s2inv]
-		elif sd == 'seccw':
-			offset = [s2inv, -s2]
-		elif sd == 'eccw':
-			offset = [-s2inv, -s2]
-		elif sd == 'neccw':
-			offset = [-s2, -s2inv]
-
-		path += " ".join(str(x) for x in offset)
-		self.attrs['d'] += path
-		return self
-
-	def arc(self, sweep):
-		x = AR
-		y = AR
-		if sweep[0] == 'e' or sweep[1] == 'w':
-			x *= -1
-		if sweep[0] == 's' or sweep[1] == 'n':
-			y *= -1
-		cw = 1 if sweep == 'ne' or sweep == 'es' or sweep == 'sw' or sweep == 'wn' else 0
-		self.attrs['d'] += 'a{0} {0} 0 0 {1} {2} {3}'.format(AR, cw, x, y)
-		return self
-
-
-	def format(self):
-		self.attrs['d'] += 'h.5'
-		return self
-
-	def __repr__(self):
-		return 'Path(%r, %r)' % (self.x, self.y)
-
-
-def wrapString(value):
-	return value if isinstance(value, DiagramItem) else Terminal(value)
-
-
-DEFAULT_STYLE = '''\
+    def __init__(
+        self,
+        name: str,
+        items: Seq[Node],
+        attrs: Opt[Dict[str, str]] = None,
+        text: Opt[str] = None,
+    ):
+        DiagramItem.__init__(self, name, attrs, text)
+        self.items: List[DiagramItem] = [wrapString(item) for item in items]
+
+    def format(self, x: float, y: float, width: float) -> DiagramItem:
+        raise NotImplementedError  # Virtual
+
+    def walk(self, cb: WalkerF) -> None:
+        cb(self)
+        for item in self.items:
+            item.walk(cb)
+
+
+class Path:
+    def __init__(self, x: float, y: float):
+        self.x = x
+        self.y = y
+        self.attrs = {"d": f"M{x} {y}"}
+
+    def m(self, x: float, y: float) -> Path:
+        self.attrs["d"] += f"m{x} {y}"
+        return self
+
+    def l(self, x: float, y: float) -> Path:
+        self.attrs["d"] += f"l{x} {y}"
+        return self
+
+    def h(self, val: float) -> Path:
+        self.attrs["d"] += f"h{val}"
+        return self
+
+    def right(self, val: float) -> Path:
+        return self.h(max(0, val))
+
+    def left(self, val: float) -> Path:
+        return self.h(-max(0, val))
+
+    def v(self, val: float) -> Path:
+        self.attrs["d"] += f"v{val}"
+        return self
+
+    def down(self, val: float) -> Path:
+        return self.v(max(0, val))
+
+    def up(self, val: float) -> Path:
+        return self.v(-max(0, val))
+
+    def arc_8(self, start: str, dir: str) -> Path:
+        # 1/8 of a circle
+        arc = AR
+        s2 = 1 / Math.sqrt(2) * arc
+        s2inv = arc - s2
+        sweep = "1" if dir == "cw" else "0"
+        path = f"a {arc} {arc} 0 0 {sweep} "
+        sd = start + dir
+        offset: List[float]
+        if sd == "ncw":
+            offset = [s2, s2inv]
+        elif sd == "necw":
+            offset = [s2inv, s2]
+        elif sd == "ecw":
+            offset = [-s2inv, s2]
+        elif sd == "secw":
+            offset = [-s2, s2inv]
+        elif sd == "scw":
+            offset = [-s2, -s2inv]
+        elif sd == "swcw":
+            offset = [-s2inv, -s2]
+        elif sd == "wcw":
+            offset = [s2inv, -s2]
+        elif sd == "nwcw":
+            offset = [s2, -s2inv]
+        elif sd == "nccw":
+            offset = [-s2, s2inv]
+        elif sd == "nwccw":
+            offset = [-s2inv, s2]
+        elif sd == "wccw":
+            offset = [s2inv, s2]
+        elif sd == "swccw":
+            offset = [s2, s2inv]
+        elif sd == "sccw":
+            offset = [s2, -s2inv]
+        elif sd == "seccw":
+            offset = [s2inv, -s2]
+        elif sd == "eccw":
+            offset = [-s2inv, -s2]
+        elif sd == "neccw":
+            offset = [-s2, -s2inv]
+
+        path += " ".join(str(x) for x in offset)
+        self.attrs["d"] += path
+        return self
+
+    def arc(self, sweep: str) -> Path:
+        x = AR
+        y = AR
+        if sweep[0] == "e" or sweep[1] == "w":
+            x *= -1
+        if sweep[0] == "s" or sweep[1] == "n":
+            y *= -1
+        cw = 1 if sweep in ("ne", "es", "sw", "wn") else 0
+        self.attrs["d"] += f"a{AR} {AR} 0 0 {cw} {x} {y}"
+        return self
+
+    def addTo(self, parent: DiagramItem) -> Path:
+        parent.children.append(self)
+        return self
+
+    def writeSvg(self, write: WriterF) -> None:
+        write("<path")
+        for name, value in sorted(self.attrs.items()):
+            write(f' {name}="{escapeAttr(value)}"')
+        write(" />")
+
+    def format(self) -> Path:
+        self.attrs["d"] += "h.5"
+        return self
+
+    def __repr__(self) -> str:
+        return f"Path({repr(self.x)}, {repr(self.y)})"
+
+
+def wrapString(value: Node) -> DiagramItem:
+    return value if isinstance(value, DiagramItem) else Terminal(value)
+
+
+DEFAULT_STYLE = """\
 	svg.railroad-diagram {
 		background-color:hsl(30,20%,95%);
 	}
@@ -226,1052 +283,1161 @@ DEFAULT_STYLE = '''\
 		stroke-dasharray: 10 5;
 		fill: none;
 	}
-'''
+"""
 
 
-class Style(DiagramItem):
-	def __init__(self, css):
-		self.name = 'style'
-		self.css = css
-		self.height = 0
-		self.width = 0
-		self.needsSpace = False
+class Style:
+    def __init__(self, css: str):
+        self.css = css
 
-	def __repr__(self):
-		return 'Style(%r)' % self.css
+    def __repr__(self) -> str:
+        return f"Style({repr(self.css)})"
 
-	def format(self, x, y, width):
-		return self
+    def addTo(self, parent: DiagramItem) -> Style:
+        parent.children.append(self)
+        return self
 
-	def writeSvg(self, write):
-		# Write included stylesheet as CDATA. See https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style
-		cdata = u'/* <![CDATA[ */\n{css}\n/* ]]> */\n'.format(css=self.css)
-		write(u'<style>{cdata}</style>'.format(cdata=cdata))
+    def format(self) -> Style:
+        return self
+
+    def writeSvg(self, write: WriterF) -> None:
+        # Write included stylesheet as CDATA. See https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style
+        cdata = "/* <![CDATA[ */\n{css}\n/* ]]> */\n".format(css=self.css)
+        write("<style>{cdata}</style>".format(cdata=cdata))
 
 
 class Diagram(DiagramMultiContainer):
-	def __init__(self, *items, **kwargs):
-		# Accepts a type=[simple|complex] kwarg
-		DiagramMultiContainer.__init__(
-			self,
-			'svg',
-			items,
-			{'class': DIAGRAM_CLASS, 'xmlns': "http://www.w3.org/2000/svg",
-				'xmlns:xlink': "http://www.w3.org/1999/xlink"}
-		)
-		self.type = kwargs.get("type", "simple")
-		if items and not isinstance(items[0], Start):
-			self.items.insert(0, Start(self.type))
-		if items and not isinstance(items[-1], End):
-			self.items.append(End(self.type))
-		self.css = kwargs.get("css", DEFAULT_STYLE)
-		if self.css:
-			self.items.insert(0, Style(self.css))
-		self.up = 0
-		self.down = 0
-		self.height = 0
-		self.width = 0
-		for item in self.items:
-			if isinstance(item, Style):
-				continue
-			self.width += item.width + (20 if item.needsSpace else 0)
-			self.up = max(self.up, item.up - self.height)
-			self.height += item.height
-			self.down = max(self.down - item.height, item.down)
-		if self.items[0].needsSpace:
-			self.width -= 10
-		if self.items[-1].needsSpace:
-			self.width -= 10
-		self.formatted = False
-
-	def __repr__(self):
-		if self.css:
-			items = ', '.join(map(repr, self.items[2:-1]))
-		else:
-			items = ', '.join(map(repr, self.items[1:-1]))
-		pieces = [] if not items else [items]
-		if self.css != DEFAULT_STYLE:
-			pieces.append('css=%r' % self.css)
-		if self.type != 'simple':
-			pieces.append('type=%r' % self.type)
-		return 'Diagram(%s)' % ', '.join(pieces)
-
-	def format(self, paddingTop=20, paddingRight=None, paddingBottom=None, paddingLeft=None):
-		if paddingRight is None:
-			paddingRight = paddingTop
-		if paddingBottom is None:
-			paddingBottom = paddingTop
-		if paddingLeft is None:
-			paddingLeft = paddingRight
-		x = paddingLeft
-		y = paddingTop + self.up
-		g = DiagramItem('g')
-		if STROKE_ODD_PIXEL_LENGTH:
-			g.attrs['transform'] = 'translate(.5 .5)'
-		for item in self.items:
-			if item.needsSpace:
-				Path(x, y).h(10).addTo(g)
-				x += 10
-			item.format(x, y, item.width).addTo(g)
-			x += item.width
-			y += item.height
-			if item.needsSpace:
-				Path(x, y).h(10).addTo(g)
-				x += 10
-		self.attrs['width'] = self.width + paddingLeft + paddingRight
-		self.attrs['height'] = self.up + self.height + self.down + paddingTop + paddingBottom
-		self.attrs['viewBox'] = "0 0 {width} {height}".format(**self.attrs)
-		g.addTo(self)
-		self.formatted = True
-		return self
-
-
-	def writeSvg(self, write):
-		if not self.formatted:
-			self.format()
-		return DiagramItem.writeSvg(self, write)
-
-	def parseCSSGrammar(self, text):
-		token_patterns = {
-			'keyword': r"[\w-]+\(?",
-			'type': r"<[\w-]+(\(\))?>",
-			'char': r"[/,()]",
-			'literal': r"'(.)'",
-			'openbracket': r"\[",
-			'closebracket': r"\]",
-			'closebracketbang': r"\]!",
-			'bar': r"\|",
-			'doublebar': r"\|\|",
-			'doubleand': r"&&",
-			'multstar': r"\*",
-			'multplus': r"\+",
-			'multhash': r"#",
-			'multnum1': r"{\s*(\d+)\s*}",
-			'multnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}",
-			'multhashnum1': r"#{\s*(\d+)\s*}",
-			'multhashnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}"
-		}
+    def __init__(self, *items: Node, **kwargs: str):
+        # Accepts a type=[simple|complex] kwarg
+        DiagramMultiContainer.__init__(
+            self,
+            "svg",
+            list(items),
+            {
+                "class": DIAGRAM_CLASS,
+            },
+        )
+        self.type = kwargs.get("type", "simple")
+        if items and not isinstance(items[0], Start):
+            self.items.insert(0, Start(self.type))
+        if items and not isinstance(items[-1], End):
+            self.items.append(End(self.type))
+        self.up = 0
+        self.down = 0
+        self.height = 0
+        self.width = 0
+        for item in self.items:
+            if isinstance(item, Style):
+                continue
+            self.width += item.width + (20 if item.needsSpace else 0)
+            self.up = max(self.up, item.up - self.height)
+            self.height += item.height
+            self.down = max(self.down - item.height, item.down)
+        if self.items[0].needsSpace:
+            self.width -= 10
+        if self.items[-1].needsSpace:
+            self.width -= 10
+        self.formatted = False
+
+    def __repr__(self) -> str:
+        items = ", ".join(map(repr, self.items[1:-1]))
+        pieces = [] if not items else [items]
+        if self.type != "simple":
+            pieces.append(f"type={repr(self.type)}")
+        return f'Diagram({", ".join(pieces)})'
+
+    def format(
+        self,
+        paddingTop: float = 20,
+        paddingRight: Opt[float] = None,
+        paddingBottom: Opt[float] = None,
+        paddingLeft: Opt[float] = None,
+    ) -> Diagram:
+        if paddingRight is None:
+            paddingRight = paddingTop
+        if paddingBottom is None:
+            paddingBottom = paddingTop
+        if paddingLeft is None:
+            paddingLeft = paddingRight
+        assert paddingRight is not None
+        assert paddingBottom is not None
+        assert paddingLeft is not None
+        x = paddingLeft
+        y = paddingTop + self.up
+        g = DiagramItem("g")
+        if STROKE_ODD_PIXEL_LENGTH:
+            g.attrs["transform"] = "translate(.5 .5)"
+        for item in self.items:
+            if item.needsSpace:
+                Path(x, y).h(10).addTo(g)
+                x += 10
+            item.format(x, y, item.width).addTo(g)
+            x += item.width
+            y += item.height
+            if item.needsSpace:
+                Path(x, y).h(10).addTo(g)
+                x += 10
+        self.attrs["width"] = str(self.width + paddingLeft + paddingRight)
+        self.attrs["height"] = str(
+            self.up + self.height + self.down + paddingTop + paddingBottom
+        )
+        self.attrs["viewBox"] = f"0 0 {self.attrs['width']} {self.attrs['height']}"
+        g.addTo(self)
+        self.formatted = True
+        return self
+
+    def writeSvg(self, write: WriterF) -> None:
+        if not self.formatted:
+            self.format()
+        return DiagramItem.writeSvg(self, write)
+
+    def writeStandalone(self, write: WriterF, css: str|None = None) -> None:
+        if not self.formatted:
+            self.format()
+        if css is None:
+            css = DEFAULT_STYLE
+        Style(css).addTo(self)
+        self.attrs["xmlns"] = "http://www.w3.org/2000/svg"
+        self.attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink"
+        DiagramItem.writeSvg(self, write)
+        self.children.pop()
+        del self.attrs["xmlns"]
+        del self.attrs["xmlns:xlink"]
 
 
 class Sequence(DiagramMultiContainer):
-	def __init__(self, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		self.needsSpace = True
-		self.up = 0
-		self.down = 0
-		self.height = 0
-		self.width = 0
-		for item in self.items:
-			self.width += item.width + (20 if item.needsSpace else 0)
-			self.up = max(self.up, item.up - self.height)
-			self.height += item.height
-			self.down = max(self.down - item.height, item.down)
-		if self.items[0].needsSpace:
-			self.width -= 10
-		if self.items[-1].needsSpace:
-			self.width -= 10
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(map(repr, self.items))
-		return 'Sequence(%s)' % items
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x+leftGap+self.width, y+self.height).h(rightGap).addTo(self)
-		x += leftGap
-		for i,item in enumerate(self.items):
-			if item.needsSpace and i > 0:
-				Path(x, y).h(10).addTo(self)
-				x += 10
-			item.format(x, y, item.width).addTo(self)
-			x += item.width
-			y += item.height
-			if item.needsSpace and i < len(self.items)-1:
-				Path(x, y).h(10).addTo(self)
-				x += 10
-		return self
+    def __init__(self, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        self.needsSpace = True
+        self.up = 0
+        self.down = 0
+        self.height = 0
+        self.width = 0
+        for item in self.items:
+            self.width += item.width + (20 if item.needsSpace else 0)
+            self.up = max(self.up, item.up - self.height)
+            self.height += item.height
+            self.down = max(self.down - item.height, item.down)
+        if self.items[0].needsSpace:
+            self.width -= 10
+        if self.items[-1].needsSpace:
+            self.width -= 10
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return f"Sequence({items})"
+
+    def format(self, x: float, y: float, width: float) -> Sequence:
+        leftGap, rightGap = determineGaps(width, self.width)
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+        for i, item in enumerate(self.items):
+            if item.needsSpace and i > 0:
+                Path(x, y).h(10).addTo(self)
+                x += 10
+            item.format(x, y, item.width).addTo(self)
+            x += item.width
+            y += item.height
+            if item.needsSpace and i < len(self.items) - 1:
+                Path(x, y).h(10).addTo(self)
+                x += 10
+        return self
 
 
 class Stack(DiagramMultiContainer):
-	def __init__(self, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		self.needsSpace = True
-		self.width = max(item.width + (20 if item.needsSpace else 0) for item in self.items)
-		# pretty sure that space calc is totes wrong
-		if len(self.items) > 1:
-			self.width += AR*2
-		self.up = self.items[0].up
-		self.down = self.items[-1].down
-		self.height = 0
-		last = len(self.items) - 1
-		for i,item in enumerate(self.items):
-			self.height += item.height
-			if i > 0:
-				self.height += max(AR*2, item.up + VS)
-			if i < last:
-				self.height += max(AR*2, item.down + VS)
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(repr(item) for item in self.items)
-		return 'Stack(%s)' % items
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-		Path(x, y).h(leftGap).addTo(self)
-		x += leftGap
-		xInitial = x
-		if len(self.items) > 1:
-			Path(x, y).h(AR).addTo(self)
-			x += AR
-			innerWidth = self.width - AR*2
-		else:
-			innerWidth = self.width
-		for i,item in enumerate(self.items):
-			item.format(x, y, innerWidth).addTo(self)
-			x += innerWidth
-			y += item.height
-			if i != len(self.items)-1:
-				(Path(x,y)
-					.arc('ne').down(max(0, item.down + VS - AR*2))
-					.arc('es').left(innerWidth)
-					.arc('nw').down(max(0, self.items[i+1].up + VS - AR*2))
-					.arc('ws').addTo(self))
-				y += max(item.down + VS, AR*2) + max(self.items[i+1].up + VS, AR*2)
-				x = xInitial + AR
-		if len(self.items) > 1:
-			Path(x, y).h(AR).addTo(self)
-			x += AR
-		Path(x, y).h(rightGap).addTo(self)
-		return self
+    def __init__(self, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        self.needsSpace = True
+        self.width = max(
+            item.width + (20 if item.needsSpace else 0) for item in self.items
+        )
+        # pretty sure that space calc is totes wrong
+        if len(self.items) > 1:
+            self.width += AR * 2
+        self.up = self.items[0].up
+        self.down = self.items[-1].down
+        self.height = 0
+        last = len(self.items) - 1
+        for i, item in enumerate(self.items):
+            self.height += item.height
+            if i > 0:
+                self.height += max(AR * 2, item.up + VS)
+            if i < last:
+                self.height += max(AR * 2, item.down + VS)
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return f"Stack({items})"
+
+    def format(self, x: float, y: float, width: float) -> Stack:
+        leftGap, rightGap = determineGaps(width, self.width)
+        Path(x, y).h(leftGap).addTo(self)
+        x += leftGap
+        xInitial = x
+        if len(self.items) > 1:
+            Path(x, y).h(AR).addTo(self)
+            x += AR
+            innerWidth = self.width - AR * 2
+        else:
+            innerWidth = self.width
+        for i, item in enumerate(self.items):
+            item.format(x, y, innerWidth).addTo(self)
+            x += innerWidth
+            y += item.height
+            if i != len(self.items) - 1:
+                (
+                    Path(x, y)
+                    .arc("ne")
+                    .down(max(0, item.down + VS - AR * 2))
+                    .arc("es")
+                    .left(innerWidth)
+                    .arc("nw")
+                    .down(max(0, self.items[i + 1].up + VS - AR * 2))
+                    .arc("ws")
+                    .addTo(self)
+                )
+                y += max(item.down + VS, AR * 2) + max(
+                    self.items[i + 1].up + VS, AR * 2
+                )
+                x = xInitial + AR
+        if len(self.items) > 1:
+            Path(x, y).h(AR).addTo(self)
+            x += AR
+        Path(x, y).h(rightGap).addTo(self)
+        return self
 
 
 class OptionalSequence(DiagramMultiContainer):
-	def __new__(cls, *items):
-		if len(items) <= 1:
-			return Sequence(*items)
-		else:
-			return super(OptionalSequence, cls).__new__(cls)
-
-	def __init__(self, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		self.needsSpace = False
-		self.width = 0
-		self.up = 0
-		self.height = sum(item.height for item in self.items)
-		self.down = self.items[0].down
-		heightSoFar = 0
-		for i,item in enumerate(self.items):
-			self.up = max(self.up, max(AR * 2, item.up + VS) - heightSoFar)
-			heightSoFar += item.height
-			if i > 0:
-				self.down = max(self.height + self.down, heightSoFar + max(AR*2, item.down + VS)) - self.height
-			itemWidth = item.width + (10 if item.needsSpace else 0)
-			if i == 0:
-				self.width += AR + max(itemWidth, AR)
-			else:
-				self.width += AR*2 + max(itemWidth, AR) + AR
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(repr(item) for item in self.items)
-		return 'OptionalSequence(%s)' % items
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-		Path(x, y).right(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y + self.height).right(rightGap).addTo(self)
-		x += leftGap
-		upperLineY = y - self.up
-		last = len(self.items) - 1
-		for i,item in enumerate(self.items):
-			itemSpace = 10 if item.needsSpace else 0
-			itemWidth = item.width + itemSpace
-			if i == 0:
-				# Upper skip
-				(Path(x,y)
-					.arc('se')
-					.up(y - upperLineY - AR*2)
-					.arc('wn')
-					.right(itemWidth - AR)
-					.arc('ne')
-					.down(y + item.height - upperLineY - AR*2)
-					.arc('ws')
-					.addTo(self))
-				# Straight line
-				(Path(x, y)
-					.right(itemSpace + AR)
-					.addTo(self))
-				item.format(x + itemSpace + AR, y, item.width).addTo(self)
-				x += itemWidth + AR
-				y += item.height
-			elif i < last:
-				# Upper skip
-				(Path(x, upperLineY)
-					.right(AR*2 + max(itemWidth, AR) + AR)
-					.arc('ne')
-					.down(y - upperLineY + item.height - AR*2)
-					.arc('ws')
-					.addTo(self))
-				# Straight line
-				(Path(x,y)
-					.right(AR*2)
-					.addTo(self))
-				item.format(x + AR*2, y, item.width).addTo(self)
-				(Path(x + item.width + AR*2, y + item.height)
-					.right(itemSpace + AR)
-					.addTo(self))
-				# Lower skip
-				(Path(x,y)
-					.arc('ne')
-					.down(item.height + max(item.down + VS, AR*2) - AR*2)
-					.arc('ws')
-					.right(itemWidth - AR)
-					.arc('se')
-					.up(item.down + VS - AR*2)
-					.arc('wn')
-					.addTo(self))
-				x += AR*2 + max(itemWidth, AR) + AR
-				y += item.height
-			else:
-				# Straight line
-				(Path(x, y)
-					.right(AR*2)
-					.addTo(self))
-				item.format(x + AR*2, y, item.width).addTo(self)
-				(Path(x + AR*2 + item.width, y + item.height)
-					.right(itemSpace + AR)
-					.addTo(self))
-				# Lower skip
-				(Path(x,y)
-					.arc('ne')
-					.down(item.height + max(item.down + VS, AR*2) - AR*2)
-					.arc('ws')
-					.right(itemWidth - AR)
-					.arc('se')
-					.up(item.down + VS - AR*2)
-					.arc('wn')
-					.addTo(self))
-		return self
+    def __new__(cls, *items: Node) -> Any:
+        if len(items) <= 1:
+            return Sequence(*items)
+        else:
+            return super(OptionalSequence, cls).__new__(cls)
+
+    def __init__(self, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        self.needsSpace = False
+        self.width = 0
+        self.up = 0
+        self.height = sum(item.height for item in self.items)
+        self.down = self.items[0].down
+        heightSoFar: float = 0
+        for i, item in enumerate(self.items):
+            self.up = max(self.up, max(AR * 2, item.up + VS) - heightSoFar)
+            heightSoFar += item.height
+            if i > 0:
+                self.down = (
+                    max(
+                        self.height + self.down,
+                        heightSoFar + max(AR * 2, item.down + VS),
+                    )
+                    - self.height
+                )
+            itemWidth = item.width + (10 if item.needsSpace else 0)
+            if i == 0:
+                self.width += AR + max(itemWidth, AR)
+            else:
+                self.width += AR * 2 + max(itemWidth, AR) + AR
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return f"OptionalSequence({items})"
+
+    def format(self, x: float, y: float, width: float) -> OptionalSequence:
+        leftGap, rightGap = determineGaps(width, self.width)
+        Path(x, y).right(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).right(rightGap).addTo(self)
+        x += leftGap
+        upperLineY = y - self.up
+        last = len(self.items) - 1
+        for i, item in enumerate(self.items):
+            itemSpace = 10 if item.needsSpace else 0
+            itemWidth = item.width + itemSpace
+            if i == 0:
+                # Upper skip
+                (
+                    Path(x, y)
+                    .arc("se")
+                    .up(y - upperLineY - AR * 2)
+                    .arc("wn")
+                    .right(itemWidth - AR)
+                    .arc("ne")
+                    .down(y + item.height - upperLineY - AR * 2)
+                    .arc("ws")
+                    .addTo(self)
+                )
+                # Straight line
+                (Path(x, y).right(itemSpace + AR).addTo(self))
+                item.format(x + itemSpace + AR, y, item.width).addTo(self)
+                x += itemWidth + AR
+                y += item.height
+            elif i < last:
+                # Upper skip
+                (
+                    Path(x, upperLineY)
+                    .right(AR * 2 + max(itemWidth, AR) + AR)
+                    .arc("ne")
+                    .down(y - upperLineY + item.height - AR * 2)
+                    .arc("ws")
+                    .addTo(self)
+                )
+                # Straight line
+                (Path(x, y).right(AR * 2).addTo(self))
+                item.format(x + AR * 2, y, item.width).addTo(self)
+                (
+                    Path(x + item.width + AR * 2, y + item.height)
+                    .right(itemSpace + AR)
+                    .addTo(self)
+                )
+                # Lower skip
+                (
+                    Path(x, y)
+                    .arc("ne")
+                    .down(item.height + max(item.down + VS, AR * 2) - AR * 2)
+                    .arc("ws")
+                    .right(itemWidth - AR)
+                    .arc("se")
+                    .up(item.down + VS - AR * 2)
+                    .arc("wn")
+                    .addTo(self)
+                )
+                x += AR * 2 + max(itemWidth, AR) + AR
+                y += item.height
+            else:
+                # Straight line
+                (Path(x, y).right(AR * 2).addTo(self))
+                item.format(x + AR * 2, y, item.width).addTo(self)
+                (
+                    Path(x + AR * 2 + item.width, y + item.height)
+                    .right(itemSpace + AR)
+                    .addTo(self)
+                )
+                # Lower skip
+                (
+                    Path(x, y)
+                    .arc("ne")
+                    .down(item.height + max(item.down + VS, AR * 2) - AR * 2)
+                    .arc("ws")
+                    .right(itemWidth - AR)
+                    .arc("se")
+                    .up(item.down + VS - AR * 2)
+                    .arc("wn")
+                    .addTo(self)
+                )
+        return self
+
 
 class AlternatingSequence(DiagramMultiContainer):
-	def __new__(cls, *items):
-		if len(items) == 2:
-			return super(AlternatingSequence, cls).__new__(cls)
-		else:
-			raise Exception("AlternatingSequence takes exactly two arguments got " + len(items))
-
-	def __init__(self, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		self.needsSpace = False
-
-		arc = AR
-		vert = VS
-		first = self.items[0]
-		second = self.items[1]
-
-		arcX = 1 / Math.sqrt(2) * arc * 2
-		arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
-		crossY = max(arc, vert)
-		crossX = (crossY - arcY) + arcX
-
-		firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down)
-		self.up = firstOut + first.height + first.up
-
-		secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up)
-		self.down = secondIn + second.height + second.down
-
-		self.height = 0
-
-		firstWidth = (20 if first.needsSpace else 0) + first.width
-		secondWidth = (20 if second.needsSpace else 0) + second.width
-		self.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(repr(item) for item in self.items)
-		return 'AlternatingSequence(%s)' % items
-
-	def format(self, x, y, width):
-		arc = AR
-		gaps = determineGaps(width, self.width)
-		Path(x,y).right(gaps[0]).addTo(self)
-		x += gaps[0]
-		Path(x+self.width, y).right(gaps[1]).addTo(self)
-		# bounding box
-		# Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self)
-		first = self.items[0]
-		second = self.items[1]
-
-		# top
-		firstIn = self.up - first.up
-		firstOut = self.up - first.up - first.height
-		Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(self)
-		first.format(x + 2*arc, y - firstIn, self.width - 4*arc).addTo(self)
-		Path(x + self.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(self)
-
-		# bottom
-		secondIn = self.down - second.down - second.height
-		secondOut = self.down - second.down
-		Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(self)
-		second.format(x + 2*arc, y + secondIn, self.width - 4*arc).addTo(self)
-		Path(x + self.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(self)
-
-		# crossover
-		arcX = 1 / Math.sqrt(2) * arc * 2
-		arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
-		crossY = max(arc, VS)
-		crossX = (crossY - arcY) + arcX
-		crossBar = (self.width - 4*arc - crossX)/2
-		(Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar)
-			.arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw')
-			.right(crossBar).arc('ne').addTo(self))
-		(Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar)
-			.arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw')
-			.right(crossBar).arc('se').addTo(self))
-
-		return self
+    def __new__(cls, *items: Node) -> AlternatingSequence:
+        if len(items) == 2:
+            return super(AlternatingSequence, cls).__new__(cls)
+        else:
+            raise Exception(
+                "AlternatingSequence takes exactly two arguments, but got {0} arguments.".format(
+                    len(items)
+                )
+            )
+
+    def __init__(self, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        self.needsSpace = False
+
+        arc = AR
+        vert = VS
+        first = self.items[0]
+        second = self.items[1]
+
+        arcX = 1 / Math.sqrt(2) * arc * 2
+        arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
+        crossY = max(arc, vert)
+        crossX = (crossY - arcY) + arcX
+
+        firstOut = max(
+            arc + arc, crossY / 2 + arc + arc, crossY / 2 + vert + first.down
+        )
+        self.up = firstOut + first.height + first.up
+
+        secondIn = max(arc + arc, crossY / 2 + arc + arc, crossY / 2 + vert + second.up)
+        self.down = secondIn + second.height + second.down
+
+        self.height = 0
+
+        firstWidth = (20 if first.needsSpace else 0) + first.width
+        secondWidth = (20 if second.needsSpace else 0) + second.width
+        self.width = 2 * arc + max(firstWidth, crossX, secondWidth) + 2 * arc
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return f"AlternatingSequence({items})"
+
+    def format(self, x: float, y: float, width: float) -> AlternatingSequence:
+        arc = AR
+        gaps = determineGaps(width, self.width)
+        Path(x, y).right(gaps[0]).addTo(self)
+        x += gaps[0]
+        Path(x + self.width, y).right(gaps[1]).addTo(self)
+        # bounding box
+        # Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self)
+        first = self.items[0]
+        second = self.items[1]
+
+        # top
+        firstIn = self.up - first.up
+        firstOut = self.up - first.up - first.height
+        Path(x, y).arc("se").up(firstIn - 2 * arc).arc("wn").addTo(self)
+        first.format(x + 2 * arc, y - firstIn, self.width - 4 * arc).addTo(self)
+        Path(x + self.width - 2 * arc, y - firstOut).arc("ne").down(
+            firstOut - 2 * arc
+        ).arc("ws").addTo(self)
+
+        # bottom
+        secondIn = self.down - second.down - second.height
+        secondOut = self.down - second.down
+        Path(x, y).arc("ne").down(secondIn - 2 * arc).arc("ws").addTo(self)
+        second.format(x + 2 * arc, y + secondIn, self.width - 4 * arc).addTo(self)
+        Path(x + self.width - 2 * arc, y + secondOut).arc("se").up(
+            secondOut - 2 * arc
+        ).arc("wn").addTo(self)
+
+        # crossover
+        arcX = 1 / Math.sqrt(2) * arc * 2
+        arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
+        crossY = max(arc, VS)
+        crossX = (crossY - arcY) + arcX
+        crossBar = (self.width - 4 * arc - crossX) / 2
+        (
+            Path(x + arc, y - crossY / 2 - arc)
+            .arc("ws")
+            .right(crossBar)
+            .arc_8("n", "cw")
+            .l(crossX - arcX, crossY - arcY)
+            .arc_8("sw", "ccw")
+            .right(crossBar)
+            .arc("ne")
+            .addTo(self)
+        )
+        (
+            Path(x + arc, y + crossY / 2 + arc)
+            .arc("wn")
+            .right(crossBar)
+            .arc_8("s", "ccw")
+            .l(crossX - arcX, -(crossY - arcY))
+            .arc_8("nw", "cw")
+            .right(crossBar)
+            .arc("se")
+            .addTo(self)
+        )
+
+        return self
 
 
 class Choice(DiagramMultiContainer):
-	def __init__(self, default, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		assert default < len(items)
-		self.default = default
-		self.width = AR * 4 + max(item.width for item in self.items)
-		self.up = self.items[0].up
-		self.down = self.items[-1].down
-		self.height = self.items[default].height
-		for i, item in enumerate(self.items):
-			if i in [default-1, default+1]:
-				arcs = AR*2
-			else:
-				arcs = AR
-			if i < default:
-				self.up += max(arcs, item.height + item.down + VS + self.items[i+1].up)
-			elif i == default:
-				continue
-			else:
-				self.down += max(arcs, item.up + VS + self.items[i-1].down + self.items[i-1].height)
-		self.down -= self.items[default].height # already counted in self.height
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(repr(item) for item in self.items)
-		return 'Choice(%r, %s)' % (self.default, items)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
-		x += leftGap
-
-		innerWidth = self.width - AR * 4
-		default = self.items[self.default]
-
-		# Do the elements that curve above
-		above = self.items[:self.default][::-1]
-		if above:
-			distanceFromY = max(
-				AR * 2,
-				default.up
-					+ VS
-					+ above[0].down
-					+ above[0].height)
-		for i,ni,item in doubleenumerate(above):
-			Path(x, y).arc('se').up(distanceFromY - AR * 2).arc('wn').addTo(self)
-			item.format(x + AR * 2, y - distanceFromY, innerWidth).addTo(self)
-			Path(x + AR * 2 + innerWidth, y - distanceFromY + item.height).arc('ne') \
-				.down(distanceFromY - item.height + default.height - AR*2).arc('ws').addTo(self)
-			if ni < -1:
-				distanceFromY += max(
-					AR,
-					item.up
-						+ VS
-						+ above[i+1].down
-						+ above[i+1].height)
-
-		# Do the straight-line path.
-		Path(x, y).right(AR * 2).addTo(self)
-		self.items[self.default].format(x + AR * 2, y, innerWidth).addTo(self)
-		Path(x + AR * 2 + innerWidth, y+self.height).right(AR * 2).addTo(self)
-
-		# Do the elements that curve below
-		below = self.items[self.default + 1:]
-		if below:
-			distanceFromY = max(
-				AR * 2,
-				default.height
-					+ default.down
-					+ VS
-					+ below[0].up)
-		for i, item in enumerate(below):
-			Path(x, y).arc('ne').down(distanceFromY - AR * 2).arc('ws').addTo(self)
-			item.format(x + AR * 2, y + distanceFromY, innerWidth).addTo(self)
-			Path(x + AR * 2 + innerWidth, y + distanceFromY + item.height).arc('se') \
-				.up(distanceFromY - AR * 2 + item.height - default.height).arc('wn').addTo(self)
-			distanceFromY += max(
-				AR,
-				item.height
-					+ item.down
-					+ VS
-					+ (below[i + 1].up if i+1 < len(below) else 0))
-		return self
+    def __init__(self, default: int, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        assert default < len(items)
+        self.default = default
+        self.width = AR * 4 + max(item.width for item in self.items)
+        self.up = self.items[0].up
+        self.down = self.items[-1].down
+        self.height = self.items[default].height
+        for i, item in enumerate(self.items):
+            if i in [default - 1, default + 1]:
+                arcs = AR * 2
+            else:
+                arcs = AR
+            if i < default:
+                self.up += max(
+                    arcs, item.height + item.down + VS + self.items[i + 1].up
+                )
+            elif i == default:
+                continue
+            else:
+                self.down += max(
+                    arcs,
+                    item.up + VS + self.items[i - 1].down + self.items[i - 1].height,
+                )
+        self.down -= self.items[default].height  # already counted in self.height
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return "Choice(%r, %s)" % (self.default, items)
+
+    def format(self, x: float, y: float, width: float) -> Choice:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+
+        innerWidth = self.width - AR * 4
+        default = self.items[self.default]
+
+        # Do the elements that curve above
+        above = self.items[: self.default][::-1]
+        if above:
+            distanceFromY = max(
+                AR * 2, default.up + VS + above[0].down + above[0].height
+            )
+        for i, ni, item in doubleenumerate(above):
+            Path(x, y).arc("se").up(distanceFromY - AR * 2).arc("wn").addTo(self)
+            item.format(x + AR * 2, y - distanceFromY, innerWidth).addTo(self)
+            Path(x + AR * 2 + innerWidth, y - distanceFromY + item.height).arc(
+                "ne"
+            ).down(distanceFromY - item.height + default.height - AR * 2).arc(
+                "ws"
+            ).addTo(
+                self
+            )
+            if ni < -1:
+                distanceFromY += max(
+                    AR, item.up + VS + above[i + 1].down + above[i + 1].height
+                )
+
+        # Do the straight-line path.
+        Path(x, y).right(AR * 2).addTo(self)
+        self.items[self.default].format(x + AR * 2, y, innerWidth).addTo(self)
+        Path(x + AR * 2 + innerWidth, y + self.height).right(AR * 2).addTo(self)
+
+        # Do the elements that curve below
+        below = self.items[self.default + 1 :]
+        if below:
+            distanceFromY = max(
+                AR * 2, default.height + default.down + VS + below[0].up
+            )
+        for i, item in enumerate(below):
+            Path(x, y).arc("ne").down(distanceFromY - AR * 2).arc("ws").addTo(self)
+            item.format(x + AR * 2, y + distanceFromY, innerWidth).addTo(self)
+            Path(x + AR * 2 + innerWidth, y + distanceFromY + item.height).arc("se").up(
+                distanceFromY - AR * 2 + item.height - default.height
+            ).arc("wn").addTo(self)
+            distanceFromY += max(
+                AR,
+                item.height
+                + item.down
+                + VS
+                + (below[i + 1].up if i + 1 < len(below) else 0),
+            )
+        return self
 
 
 class MultipleChoice(DiagramMultiContainer):
-	def __init__(self, default, type, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		assert 0 <= default < len(items)
-		assert type in ["any", "all"]
-		self.default = default
-		self.type = type
-		self.needsSpace = True
-		self.innerWidth = max(item.width for item in self.items)
-		self.width = 30 + AR + self.innerWidth + AR + 20
-		self.up = self.items[0].up
-		self.down = self.items[-1].down
-		self.height = self.items[default].height
-		for i, item in enumerate(self.items):
-			if i in [default-1, default+1]:
-				minimum = 10 + AR
-			else:
-				minimum = AR
-			if i < default:
-				self.up += max(minimum, item.height + item.down + VS + self.items[i+1].up)
-			elif i == default:
-				continue
-			else:
-				self.down += max(minimum, item.up + VS + self.items[i-1].down + self.items[i-1].height)
-		self.down -= self.items[default].height # already counted in self.height
-		addDebug(self)
-
-	def __repr__(self):
-		items = ', '.join(map(repr, self.items))
-		return 'MultipleChoice(%r, %r, %s)' % (self.default, self.type, items)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
-		x += leftGap
-
-		default = self.items[self.default]
-
-		# Do the elements that curve above
-		above = self.items[:self.default][::-1]
-		if above:
-			distanceFromY = max(
-				10 + AR,
-				default.up
-					+ VS
-					+ above[0].down
-					+ above[0].height)
-		for i,ni,item in doubleenumerate(above):
-			(Path(x + 30, y)
-				.up(distanceFromY - AR)
-				.arc('wn')
-				.addTo(self))
-			item.format(x + 30 + AR, y - distanceFromY, self.innerWidth).addTo(self)
-			(Path(x + 30 + AR + self.innerWidth, y - distanceFromY + item.height)
-				.arc('ne')
-				.down(distanceFromY - item.height + default.height - AR - 10)
-				.addTo(self))
-			if ni < -1:
-				distanceFromY += max(
-					AR,
-					item.up
-						+ VS
-						+ above[i+1].down
-						+ above[i+1].height)
-
-		# Do the straight-line path.
-		Path(x + 30, y).right(AR).addTo(self)
-		self.items[self.default].format(x + 30 + AR, y, self.innerWidth).addTo(self)
-		Path(x + 30 + AR + self.innerWidth, y + self.height).right(AR).addTo(self)
-
-		# Do the elements that curve below
-		below = self.items[self.default + 1:]
-		if below:
-			distanceFromY = max(
-				10 + AR,
-				default.height
-					+ default.down
-					+ VS
-					+ below[0].up)
-		for i, item in enumerate(below):
-			(Path(x+30, y)
-				.down(distanceFromY - AR)
-				.arc('ws')
-				.addTo(self))
-			item.format(x + 30 + AR, y + distanceFromY, self.innerWidth).addTo(self)
-			(Path(x + 30 + AR + self.innerWidth, y + distanceFromY + item.height)
-				.arc('se')
-				.up(distanceFromY - AR + item.height - default.height - 10)
-				.addTo(self))
-			distanceFromY += max(
-				AR,
-				item.height
-					+ item.down
-					+ VS
-					+ (below[i + 1].up if i+1 < len(below) else 0))
-		text = DiagramItem('g', attrs={"class": "diagram-text"}).addTo(self)
-		DiagramItem('title', text="take one or more branches, once each, in any order" if self.type=="any" else "take all branches, once each, in any order").addTo(text)
-		DiagramItem('path', attrs={
-			"d": "M {x} {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(x=x+30, y=y-10),
-			"class": "diagram-text"
-			}).addTo(text)
-		DiagramItem('text', text="1+" if self.type=="any" else "all", attrs={
-			"x": x + 15,
-			"y": y + 4,
-			"class": "diagram-text"
-			}).addTo(text)
-		DiagramItem('path', attrs={
-			"d": "M {x} {y} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z".format(x=x+self.width-20, y=y-10),
-			"class": "diagram-text"
-			}).addTo(text)
-		DiagramItem('text', text=u"↺", attrs={
-			"x": x + self.width - 10,
-			"y": y + 4,
-			"class": "diagram-arrow"
-			}).addTo(text)
-		return self
+    def __init__(self, default: int, type: str, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        assert 0 <= default < len(items)
+        assert type in ["any", "all"]
+        self.default = default
+        self.type = type
+        self.needsSpace = True
+        self.innerWidth = max(item.width for item in self.items)
+        self.width = 30 + AR + self.innerWidth + AR + 20
+        self.up = self.items[0].up
+        self.down = self.items[-1].down
+        self.height = self.items[default].height
+        for i, item in enumerate(self.items):
+            if i in [default - 1, default + 1]:
+                minimum = 10 + AR
+            else:
+                minimum = AR
+            if i < default:
+                self.up += max(
+                    minimum, item.height + item.down + VS + self.items[i + 1].up
+                )
+            elif i == default:
+                continue
+            else:
+                self.down += max(
+                    minimum,
+                    item.up + VS + self.items[i - 1].down + self.items[i - 1].height,
+                )
+        self.down -= self.items[default].height  # already counted in self.height
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        items = ", ".join(repr(item) for item in self.items)
+        return f"MultipleChoice({repr(self.default)}, {repr(self.type)}, {items})"
+
+    def format(self, x: float, y: float, width: float) -> MultipleChoice:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+
+        default = self.items[self.default]
+
+        # Do the elements that curve above
+        above = self.items[: self.default][::-1]
+        if above:
+            distanceFromY = max(
+                10 + AR, default.up + VS + above[0].down + above[0].height
+            )
+        for i, ni, item in doubleenumerate(above):
+            (Path(x + 30, y).up(distanceFromY - AR).arc("wn").addTo(self))
+            item.format(x + 30 + AR, y - distanceFromY, self.innerWidth).addTo(self)
+            (
+                Path(x + 30 + AR + self.innerWidth, y - distanceFromY + item.height)
+                .arc("ne")
+                .down(distanceFromY - item.height + default.height - AR - 10)
+                .addTo(self)
+            )
+            if ni < -1:
+                distanceFromY += max(
+                    AR, item.up + VS + above[i + 1].down + above[i + 1].height
+                )
+
+        # Do the straight-line path.
+        Path(x + 30, y).right(AR).addTo(self)
+        self.items[self.default].format(x + 30 + AR, y, self.innerWidth).addTo(self)
+        Path(x + 30 + AR + self.innerWidth, y + self.height).right(AR).addTo(self)
+
+        # Do the elements that curve below
+        below = self.items[self.default + 1 :]
+        if below:
+            distanceFromY = max(
+                10 + AR, default.height + default.down + VS + below[0].up
+            )
+        for i, item in enumerate(below):
+            (Path(x + 30, y).down(distanceFromY - AR).arc("ws").addTo(self))
+            item.format(x + 30 + AR, y + distanceFromY, self.innerWidth).addTo(self)
+            (
+                Path(x + 30 + AR + self.innerWidth, y + distanceFromY + item.height)
+                .arc("se")
+                .up(distanceFromY - AR + item.height - default.height - 10)
+                .addTo(self)
+            )
+            distanceFromY += max(
+                AR,
+                item.height
+                + item.down
+                + VS
+                + (below[i + 1].up if i + 1 < len(below) else 0),
+            )
+        text = DiagramItem("g", attrs={"class": "diagram-text"}).addTo(self)
+        DiagramItem(
+            "title",
+            text="take one or more branches, once each, in any order"
+            if self.type == "any"
+            else "take all branches, once each, in any order",
+        ).addTo(text)
+        DiagramItem(
+            "path",
+            attrs={
+                "d": "M {x} {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(
+                    x=x + 30, y=y - 10
+                ),
+                "class": "diagram-text",
+            },
+        ).addTo(text)
+        DiagramItem(
+            "text",
+            text="1+" if self.type == "any" else "all",
+            attrs={"x": x + 15, "y": y + 4, "class": "diagram-text"},
+        ).addTo(text)
+        DiagramItem(
+            "path",
+            attrs={
+                "d": "M {x} {y} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z".format(
+                    x=x + self.width - 20, y=y - 10
+                ),
+                "class": "diagram-text",
+            },
+        ).addTo(text)
+        DiagramItem(
+            "text",
+            text="↺",
+            attrs={"x": x + self.width - 10, "y": y + 4, "class": "diagram-arrow"},
+        ).addTo(text)
+        return self
 
 
 class HorizontalChoice(DiagramMultiContainer):
-	def __new__(cls, *items):
-		if len(items) <= 1:
-			return Sequence(*items)
-		else:
-			return super(HorizontalChoice, cls).__new__(cls)
-
-	def __init__(self, *items):
-		DiagramMultiContainer.__init__(self, 'g', items)
-		allButLast = self.items[:-1]
-		middles = self.items[1:-1]
-		first = self.items[0]
-		last = self.items[-1]
-		self.needsSpace = False
-
-		self.width = (AR # starting track
-			+ AR*2 * (len(self.items)-1) # inbetween tracks
-			+ sum(x.width + (20 if x.needsSpace else 0) for x in self.items) #items
-			+ (AR if last.height > 0 else 0) # needs space to curve up
-			+ AR) #ending track
-
-		# Always exits at entrance height
-		self.height = 0
-
-		# All but the last have a track running above them
-		self._upperTrack = max(
-			AR*2,
-			VS,
-			max(x.up for x in allButLast) + VS
-		)
-		self.up = max(self._upperTrack, last.up)
-
-		# All but the first have a track running below them
-		# Last either straight-lines or curves up, so has different calculation
-		self._lowerTrack = max(
-			VS,
-			max(x.height+max(x.down+VS, AR*2) for x in middles) if middles else 0,
-			last.height + last.down + VS
-		)
-		if first.height < self._lowerTrack:
-			# Make sure there's at least 2*AR room between first exit and lower track
-			self._lowerTrack = max(self._lowerTrack, first.height + AR*2)
-		self.down = max(self._lowerTrack, first.height + first.down)
-
-		addDebug(self)
-
-	def format(self, x, y, width):
-		# Hook up the two sides if self is narrower than its stated width.
-		leftGap, rightGap = determineGaps(width, self.width)
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
-		x += leftGap
-
-		first = self.items[0]
-		last = self.items[-1]
-
-		# upper track
-		upperSpan = (sum(x.width+(20 if x.needsSpace else 0) for x in self.items[:-1])
-			+ (len(self.items) - 2) * AR*2
-			- AR)
-		(Path(x,y)
-			.arc('se')
-			.up(self._upperTrack - AR*2)
-			.arc('wn')
-			.h(upperSpan)
-			.addTo(self))
-
-		# lower track
-		lowerSpan = (sum(x.width+(20 if x.needsSpace else 0) for x in self.items[1:])
-			+ (len(self.items) - 2) * AR*2
-			+ (AR if last.height > 0 else 0)
-			- AR)
-		lowerStart = x + AR + first.width+(20 if first.needsSpace else 0) + AR*2
-		(Path(lowerStart, y+self._lowerTrack)
-			.h(lowerSpan)
-			.arc('se')
-			.up(self._lowerTrack - AR*2)
-			.arc('wn')
-			.addTo(self))
-
-		# Items
-		for [i, item] in enumerate(self.items):
-			# input track
-			if i == 0:
-				(Path(x,y)
-					.h(AR)
-					.addTo(self))
-				x += AR
-			else:
-				(Path(x, y - self._upperTrack)
-					.arc('ne')
-					.v(self._upperTrack - AR*2)
-					.arc('ws')
-					.addTo(self))
-				x += AR*2
-
-			# item
-			itemWidth = item.width + (20 if item.needsSpace else 0)
-			item.format(x, y, itemWidth).addTo(self)
-			x += itemWidth
-
-			# output track
-			if i == len(self.items)-1:
-				if item.height == 0:
-					(Path(x,y)
-						.h(AR)
-						.addTo(self))
-				else:
-					(Path(x,y+item.height)
-						.arc('se')
-						.addTo(self))
-			elif i == 0 and item.height > self._lowerTrack:
-				# Needs to arc up to meet the lower track, not down.
-				if item.height - self._lowerTrack >= AR*2:
-					(Path(x, y+item.height)
-						.arc('se')
-						.v(self._lowerTrack - item.height + AR*2)
-						.arc('wn')
-						.addTo(self))
-				else:
-					# Not enough space to fit two arcs
-					# so just bail and draw a straight line for now.
-					(Path(x, y+item.height)
-						.l(AR*2, self._lowerTrack - item.height)
-						.addTo(self))
-			else:
-				(Path(x, y+item.height)
-					.arc('ne')
-					.v(self._lowerTrack - item.height - AR*2)
-					.arc('ws')
-					.addTo(self))
-		return self
-
-
-def Optional(item, skip=False):
-	return Choice(0 if skip else 1, Skip(), item)
+    def __new__(cls, *items: Node) -> Any:
+        if len(items) <= 1:
+            return Sequence(*items)
+        else:
+            return super(HorizontalChoice, cls).__new__(cls)
+
+    def __init__(self, *items: Node):
+        DiagramMultiContainer.__init__(self, "g", items)
+        allButLast = self.items[:-1]
+        middles = self.items[1:-1]
+        first = self.items[0]
+        last = self.items[-1]
+        self.needsSpace = False
+
+        self.width = (
+            AR  # starting track
+            + AR * 2 * (len(self.items) - 1)  # inbetween tracks
+            + sum(x.width + (20 if x.needsSpace else 0) for x in self.items)  # items
+            + (AR if last.height > 0 else 0)  # needs space to curve up
+            + AR
+        )  # ending track
+
+        # Always exits at entrance height
+        self.height = 0
+
+        # All but the last have a track running above them
+        self._upperTrack = max(AR * 2, VS, max(x.up for x in allButLast) + VS)
+        self.up = max(self._upperTrack, last.up)
+
+        # All but the first have a track running below them
+        # Last either straight-lines or curves up, so has different calculation
+        self._lowerTrack = max(
+            VS,
+            max(x.height + max(x.down + VS, AR * 2) for x in middles) if middles else 0,
+            last.height + last.down + VS,
+        )
+        if first.height < self._lowerTrack:
+            # Make sure there's at least 2*AR room between first exit and lower track
+            self._lowerTrack = max(self._lowerTrack, first.height + AR * 2)
+        self.down = max(self._lowerTrack, first.height + first.down)
+
+        addDebug(self)
+
+    def format(self, x: float, y: float, width: float) -> HorizontalChoice:
+        # Hook up the two sides if self is narrower than its stated width.
+        leftGap, rightGap = determineGaps(width, self.width)
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+
+        first = self.items[0]
+        last = self.items[-1]
+
+        # upper track
+        upperSpan = (
+            sum(x.width + (20 if x.needsSpace else 0) for x in self.items[:-1])
+            + (len(self.items) - 2) * AR * 2
+            - AR
+        )
+        (
+            Path(x, y)
+            .arc("se")
+            .up(self._upperTrack - AR * 2)
+            .arc("wn")
+            .h(upperSpan)
+            .addTo(self)
+        )
+
+        # lower track
+        lowerSpan = (
+            sum(x.width + (20 if x.needsSpace else 0) for x in self.items[1:])
+            + (len(self.items) - 2) * AR * 2
+            + (AR if last.height > 0 else 0)
+            - AR
+        )
+        lowerStart = x + AR + first.width + (20 if first.needsSpace else 0) + AR * 2
+        (
+            Path(lowerStart, y + self._lowerTrack)
+            .h(lowerSpan)
+            .arc("se")
+            .up(self._lowerTrack - AR * 2)
+            .arc("wn")
+            .addTo(self)
+        )
+
+        # Items
+        for [i, item] in enumerate(self.items):
+            # input track
+            if i == 0:
+                (Path(x, y).h(AR).addTo(self))
+                x += AR
+            else:
+                (
+                    Path(x, y - self._upperTrack)
+                    .arc("ne")
+                    .v(self._upperTrack - AR * 2)
+                    .arc("ws")
+                    .addTo(self)
+                )
+                x += AR * 2
+
+            # item
+            itemWidth = item.width + (20 if item.needsSpace else 0)
+            item.format(x, y, itemWidth).addTo(self)
+            x += itemWidth
+
+            # output track
+            if i == len(self.items) - 1:
+                if item.height == 0:
+                    (Path(x, y).h(AR).addTo(self))
+                else:
+                    (Path(x, y + item.height).arc("se").addTo(self))
+            elif i == 0 and item.height > self._lowerTrack:
+                # Needs to arc up to meet the lower track, not down.
+                if item.height - self._lowerTrack >= AR * 2:
+                    (
+                        Path(x, y + item.height)
+                        .arc("se")
+                        .v(self._lowerTrack - item.height + AR * 2)
+                        .arc("wn")
+                        .addTo(self)
+                    )
+                else:
+                    # Not enough space to fit two arcs
+                    # so just bail and draw a straight line for now.
+                    (
+                        Path(x, y + item.height)
+                        .l(AR * 2, self._lowerTrack - item.height)
+                        .addTo(self)
+                    )
+            else:
+                (
+                    Path(x, y + item.height)
+                    .arc("ne")
+                    .v(self._lowerTrack - item.height - AR * 2)
+                    .arc("ws")
+                    .addTo(self)
+                )
+        return self
+
+
+def Optional(item: Node, skip: bool = False) -> Choice:
+    return Choice(0 if skip else 1, Skip(), item)
 
 
 class OneOrMore(DiagramItem):
-	def __init__(self, item, repeat=None):
-		DiagramItem.__init__(self, 'g')
-		self.item = wrapString(item)
-		repeat = repeat or Skip()
-		self.rep = wrapString(repeat)
-		self.width = max(self.item.width, self.rep.width) + AR * 2
-		self.height = self.item.height
-		self.up = self.item.up
-		self.down = max(
-			AR * 2,
-			self.item.down + VS + self.rep.up + self.rep.height + self.rep.down)
-		self.needsSpace = True
-		addDebug(self)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y +self.height).h(rightGap).addTo(self)
-		x += leftGap
-
-		# Draw item
-		Path(x, y).right(AR).addTo(self)
-		self.item.format(x + AR, y, self.width - AR * 2).addTo(self)
-		Path(x + self.width - AR, y + self.height).right(AR).addTo(self)
-
-		# Draw repeat arc
-		distanceFromY = max(AR*2, self.item.height + self.item.down + VS + self.rep.up)
-		Path(x + AR, y).arc('nw').down(distanceFromY - AR * 2) \
-			.arc('ws').addTo(self)
-		self.rep.format(x + AR, y + distanceFromY, self.width - AR*2).addTo(self)
-		Path(x + self.width - AR, y + distanceFromY + self.rep.height).arc('se') \
-			.up(distanceFromY - AR * 2 + self.rep.height - self.item.height).arc('en').addTo(self)
-
-		return self
-
-	def walk(self, cb):
-		cb(self)
-		self.item.walk(cb)
-		self.rep.walk(cb)
-
-	def __repr__(self):
-		return 'OneOrMore(%r, repeat=%r)' % (self.item, self.rep)
-
-
-def ZeroOrMore(item, repeat=None, skip=False):
-	result = Optional(OneOrMore(item, repeat), skip)
-	return result
-
-
+    def __init__(self, item: Node, repeat: Opt[Node] = None):
+        DiagramItem.__init__(self, "g")
+        self.item = wrapString(item)
+        repeat = repeat or Skip()
+        self.rep = wrapString(repeat)
+        self.width = max(self.item.width, self.rep.width) + AR * 2
+        self.height = self.item.height
+        self.up = self.item.up
+        self.down = max(
+            AR * 2, self.item.down + VS + self.rep.up + self.rep.height + self.rep.down
+        )
+        self.needsSpace = True
+        addDebug(self)
+
+    def format(self, x: float, y: float, width: float) -> OneOrMore:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+
+        # Draw item
+        Path(x, y).right(AR).addTo(self)
+        self.item.format(x + AR, y, self.width - AR * 2).addTo(self)
+        Path(x + self.width - AR, y + self.height).right(AR).addTo(self)
+
+        # Draw repeat arc
+        distanceFromY = max(
+            AR * 2, self.item.height + self.item.down + VS + self.rep.up
+        )
+        Path(x + AR, y).arc("nw").down(distanceFromY - AR * 2).arc("ws").addTo(self)
+        self.rep.format(x + AR, y + distanceFromY, self.width - AR * 2).addTo(self)
+        Path(x + self.width - AR, y + distanceFromY + self.rep.height).arc("se").up(
+            distanceFromY - AR * 2 + self.rep.height - self.item.height
+        ).arc("en").addTo(self)
+
+        return self
+
+    def walk(self, cb: WalkerF) -> None:
+        cb(self)
+        self.item.walk(cb)
+        self.rep.walk(cb)
+
+    def __repr__(self) -> str:
+        return f"OneOrMore({repr(self.item)}, repeat={repr(self.rep)})"
+
+
+def ZeroOrMore(item: Node, repeat: Opt[Node] = None, skip: bool = False) -> Choice:
+    result = Optional(OneOrMore(item, repeat), skip)
+    return result
 
 
 class Group(DiagramItem):
-	def __init__(self, item, label):
-		DiagramItem.__init__(self, 'g')
-		self.item = wrapString(item)
-		if isinstance(label, DiagramItem):
-			self.label = label
-		elif label:
-			self.label = Comment(label)
-		else:
-			self.label = None
-
-		self.width = max(
-			self.item.width + (20 if self.item.needsSpace else 0),
-			self.label.width if self.label else 0,
-			AR*2)
-		self.height = self.item.height
-		self.boxUp = max(self.item.up + VS, AR)
-		self.up = self.boxUp
-		if self.label:
-			self.up += self.label.up + self.label.height + self.label.down
-		self.down = max(self.item.down + VS, AR)
-		self.needsSpace = True
-		addDebug(self)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-		Path(x,y).h(leftGap).addTo(self)
-		Path(x+leftGap+self.width,y+self.height).h(rightGap).addTo(self)
-		x += leftGap
-
-		DiagramItem('rect', {
-			"x":x,
-			"y":y-self.boxUp,
-			"width":self.width,
-			"height":self.boxUp + self.height + self.down,
-			"rx": AR,
-			"ry": AR,
-			'class':'group-box',
-		}).addTo(self)
-
-		self.item.format(x,y,self.width).addTo(self)
-		if self.label:
-			self.label.format(
-				x,
-				y-(self.boxUp+self.label.down+self.label.height),
-				self.label.width).addTo(self)
-
-		return self
-
-	def walk(self, cb):
-		cb(self)
-		self.item.walk(cb)
-		self.label.walk(cb)
+    def __init__(self, item: Node, label: Opt[Node] = None):
+        DiagramItem.__init__(self, "g")
+        self.item = wrapString(item)
+        self.label: Opt[DiagramItem]
+        if isinstance(label, DiagramItem):
+            self.label = label
+        elif label:
+            self.label = Comment(label)
+        else:
+            self.label = None
+
+        self.width = max(
+            self.item.width + (20 if self.item.needsSpace else 0),
+            self.label.width if self.label else 0,
+            AR * 2,
+        )
+        self.height = self.item.height
+        self.boxUp = max(self.item.up + VS, AR)
+        self.up = self.boxUp
+        if self.label:
+            self.up += self.label.up + self.label.height + self.label.down
+        self.down = max(self.item.down + VS, AR)
+        self.needsSpace = True
+        addDebug(self)
+
+    def format(self, x: float, y: float, width: float) -> Group:
+        leftGap, rightGap = determineGaps(width, self.width)
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
+        x += leftGap
+
+        DiagramItem(
+            "rect",
+            {
+                "x": x,
+                "y": y - self.boxUp,
+                "width": self.width,
+                "height": self.boxUp + self.height + self.down,
+                "rx": AR,
+                "ry": AR,
+                "class": "group-box",
+            },
+        ).addTo(self)
+
+        self.item.format(x, y, self.width).addTo(self)
+        if self.label:
+            self.label.format(
+                x,
+                y - (self.boxUp + self.label.down + self.label.height),
+                self.label.width,
+            ).addTo(self)
+
+        return self
+
+    def walk(self, cb: WalkerF) -> None:
+        cb(self)
+        self.item.walk(cb)
+        if self.label:
+            self.label.walk(cb)
 
 
 class Start(DiagramItem):
-	def __init__(self, type="simple", label=None):
-		DiagramItem.__init__(self, 'g')
-		if label:
-			self.width = max(20, len(label) * CHAR_WIDTH + 10)
-		else:
-			self.width = 20
-		self.up = 10
-		self.down = 10
-		self.type = type
-		self.label = label
-		addDebug(self)
-
-	def format(self, x, y, _width):
-		path = Path(x, y-10)
-		if self.type == "complex":
-			path.down(20).m(0, -10).right(self.width).addTo(self)
-		else:
-			path.down(20).m(10, -20).down(20).m(-10, -10).right(self.width).addTo(self)
-		if self.label:
-			DiagramItem('text', attrs={"x":x, "y":y-15, "style":"text-anchor:start"}, text=self.label).addTo(self)
-		return self
-
-	def __repr__(self):
-		return 'Start(type=%r, label=%r)' % (self.type, self.label)
+    def __init__(self, type: str = "simple", label: Opt[str] = None):
+        DiagramItem.__init__(self, "g")
+        if label:
+            self.width = max(20, len(label) * CHAR_WIDTH + 10)
+        else:
+            self.width = 20
+        self.up = 10
+        self.down = 10
+        self.type = type
+        self.label = label
+        addDebug(self)
+
+    def format(self, x: float, y: float, width: float) -> Start:
+        path = Path(x, y - 10)
+        if self.type == "complex":
+            path.down(20).m(0, -10).right(self.width).addTo(self)
+        else:
+            path.down(20).m(10, -20).down(20).m(-10, -10).right(self.width).addTo(self)
+        if self.label:
+            DiagramItem(
+                "text",
+                attrs={"x": x, "y": y - 15, "style": "text-anchor:start"},
+                text=self.label,
+            ).addTo(self)
+        return self
+
+    def __repr__(self) -> str:
+        return f"Start(type={repr(self.type)}, label={repr(self.label)})"
 
 
 class End(DiagramItem):
-	def __init__(self, type="simple"):
-		DiagramItem.__init__(self, 'path')
-		self.width = 20
-		self.up = 10
-		self.down = 10
-		self.type = type
-		addDebug(self)
+    def __init__(self, type: str = "simple"):
+        DiagramItem.__init__(self, "path")
+        self.width = 20
+        self.up = 10
+        self.down = 10
+        self.type = type
+        addDebug(self)
 
-	def format(self, x, y, _width):
-		if self.type == "simple":
-			self.attrs['d'] = 'M {0} {1} h 20 m -10 -10 v 20 m 10 -20 v 20'.format(x, y)
-		elif self.type == "complex":
-			self.attrs['d'] = 'M {0} {1} h 20 m 0 -10 v 20'.format(x, y)
-		return self
+    def format(self, x: float, y: float, width: float) -> End:
+        if self.type == "simple":
+            self.attrs["d"] = "M {0} {1} h 20 m -10 -10 v 20 m 10 -20 v 20".format(x, y)
+        elif self.type == "complex":
+            self.attrs["d"] = "M {0} {1} h 20 m 0 -10 v 20".format(x, y)
+        return self
 
-	def __repr__(self):
-		return 'End(type=%r)' % self.type
+    def __repr__(self) -> str:
+        return f"End(type={repr(self.type)})"
 
 
 class Terminal(DiagramItem):
-	def __init__(self, text, href=None, title=None, cls=""):
-		DiagramItem.__init__(self, 'g', {'class': ' '.join(['terminal', cls])})
-		self.text = text
-		self.href = href
-		self.title = title
-		self.cls = cls
-		self.width = len(text) * CHAR_WIDTH + 20
-		self.up = 11
-		self.down = 11
-		self.needsSpace = True
-		addDebug(self)
-
-	def __repr__(self):
-		return 'Terminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
-
-		DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
-							 'height': self.up + self.down, 'rx': 10, 'ry': 10}).addTo(self)
-		text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text)
-		if self.href is not None:
-			a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
-			text.addTo(a)
-		else:
-			text.addTo(self)
-		if self.title is not None:
-			DiagramItem('title', {}, self.title).addTo(self)
-		return self
+    def __init__(
+        self, text: str, href: Opt[str] = None, title: Opt[str] = None, cls: str = ""
+    ):
+        DiagramItem.__init__(self, "g", {"class": " ".join(["terminal", cls])})
+        self.text = text
+        self.href = href
+        self.title = title
+        self.cls = cls
+        self.width = len(text) * CHAR_WIDTH + 20
+        self.up = 11
+        self.down = 11
+        self.needsSpace = True
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        return f"Terminal({repr(self.text)}, href={repr(self.href)}, title={repr(self.title)}, cls={repr(self.cls)})"
+
+    def format(self, x: float, y: float, width: float) -> Terminal:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
+
+        DiagramItem(
+            "rect",
+            {
+                "x": x + leftGap,
+                "y": y - 11,
+                "width": self.width,
+                "height": self.up + self.down,
+                "rx": 10,
+                "ry": 10,
+            },
+        ).addTo(self)
+        text = DiagramItem(
+            "text", {"x": x + leftGap + self.width / 2, "y": y + 4}, self.text
+        )
+        if self.href is not None:
+            a = DiagramItem("a", {"xlink:href": self.href}, text).addTo(self)
+            text.addTo(a)
+        else:
+            text.addTo(self)
+        if self.title is not None:
+            DiagramItem("title", {}, self.title).addTo(self)
+        return self
 
 
 class NonTerminal(DiagramItem):
-	def __init__(self, text, href=None, title=None, cls=""):
-		DiagramItem.__init__(self, 'g', {'class': ' '.join(['non-terminal', cls])})
-		self.text = text
-		self.href = href
-		self.title = title
-		self.cls = cls
-		self.width = len(text) * CHAR_WIDTH + 20
-		self.up = 11
-		self.down = 11
-		self.needsSpace = True
-		addDebug(self)
-
-	def __repr__(self):
-		return 'NonTerminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
-
-		DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
-							 'height': self.up + self.down}).addTo(self)
-		text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text)
-		if self.href is not None:
-			a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
-			text.addTo(a)
-		else:
-			text.addTo(self)
-		if self.title is not None:
-			DiagramItem('title', {}, self.title).addTo(self)
-		return self
+    def __init__(
+        self, text: str, href: Opt[str] = None, title: Opt[str] = None, cls: str = ""
+    ):
+        DiagramItem.__init__(self, "g", {"class": " ".join(["non-terminal", cls])})
+        self.text = text
+        self.href = href
+        self.title = title
+        self.cls = cls
+        self.width = len(text) * CHAR_WIDTH + 20
+        self.up = 11
+        self.down = 11
+        self.needsSpace = True
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        return f"NonTerminal({repr(self.text)}, href={repr(self.href)}, title={repr(self.title)}, cls={repr(self.cls)})"
+
+    def format(self, x: float, y: float, width: float) -> NonTerminal:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
+
+        DiagramItem(
+            "rect",
+            {
+                "x": x + leftGap,
+                "y": y - 11,
+                "width": self.width,
+                "height": self.up + self.down,
+            },
+        ).addTo(self)
+        text = DiagramItem(
+            "text", {"x": x + leftGap + self.width / 2, "y": y + 4}, self.text
+        )
+        if self.href is not None:
+            a = DiagramItem("a", {"xlink:href": self.href}, text).addTo(self)
+            text.addTo(a)
+        else:
+            text.addTo(self)
+        if self.title is not None:
+            DiagramItem("title", {}, self.title).addTo(self)
+        return self
 
 
 class Comment(DiagramItem):
-	def __init__(self, text, href=None, title=None, cls=""):
-		DiagramItem.__init__(self, 'g', {'class': ' '.join(['non-terminal', cls])})
-		self.text = text
-		self.href = href
-		self.title = title
-		self.cls = cls
-		self.width = len(text) * COMMENT_CHAR_WIDTH + 10
-		self.up = 8
-		self.down = 8
-		self.needsSpace = True
-		addDebug(self)
-
-	def __repr__(self):
-		return 'Comment(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
-
-	def format(self, x, y, width):
-		leftGap, rightGap = determineGaps(width, self.width)
-
-		# Hook up the two sides if self is narrower than its stated width.
-		Path(x, y).h(leftGap).addTo(self)
-		Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
-
-		text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 5, 'class': 'comment'}, self.text)
-		if self.href is not None:
-			a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
-			text.addTo(a)
-		else:
-			text.addTo(self)
-		if self.title is not None:
-			DiagramItem('title', {}, self.title).addTo(self)
-		return self
+    def __init__(
+        self, text: str, href: Opt[str] = None, title: Opt[str] = None, cls: str = ""
+    ):
+        DiagramItem.__init__(self, "g", {"class": " ".join(["non-terminal", cls])})
+        self.text = text
+        self.href = href
+        self.title = title
+        self.cls = cls
+        self.width = len(text) * COMMENT_CHAR_WIDTH + 10
+        self.up = 8
+        self.down = 8
+        self.needsSpace = True
+        addDebug(self)
+
+    def __repr__(self) -> str:
+        return f"Comment({repr(self.text)}, href={repr(self.href)}, title={repr(self.title)}, cls={repr(self.cls)})"
+
+    def format(self, x: float, y: float, width: float) -> Comment:
+        leftGap, rightGap = determineGaps(width, self.width)
+
+        # Hook up the two sides if self is narrower than its stated width.
+        Path(x, y).h(leftGap).addTo(self)
+        Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
+
+        text = DiagramItem(
+            "text",
+            {"x": x + leftGap + self.width / 2, "y": y + 5, "class": "comment"},
+            self.text,
+        )
+        if self.href is not None:
+            a = DiagramItem("a", {"xlink:href": self.href}, text).addTo(self)
+            text.addTo(a)
+        else:
+            text.addTo(self)
+        if self.title is not None:
+            DiagramItem("title", {}, self.title).addTo(self)
+        return self
 
 
 class Skip(DiagramItem):
-	def __init__(self):
-		DiagramItem.__init__(self, 'g')
-		self.width = 0
-		self.up = 0
-		self.down = 0
-		addDebug(self)
-
-	def format(self, x, y, width):
-		Path(x, y).right(width).addTo(self)
-		return self
-
-	def __repr__(self):
-		return 'Skip()'
-
-
-if __name__ == '__main__':
-	def add(name, diagram):
-		sys.stdout.write('<h1>{0}</h1>\n'.format(e(name)))
-		diagram.writeSvg(sys.stdout.write)
-		sys.stdout.write('\n')
-
-	import sys
-	sys.stdout.write("<!doctype html><title>Test</title><body>")
-	exec(open('test.py').read())
-	sys.stdout.write('''
+    def __init__(self) -> None:
+        DiagramItem.__init__(self, "g")
+        self.width = 0
+        self.up = 0
+        self.down = 0
+        addDebug(self)
+
+    def format(self, x: float, y: float, width: float) -> Skip:
+        Path(x, y).right(width).addTo(self)
+        return self
+
+    def __repr__(self) -> str:
+        return "Skip()"
+
+
+if __name__ == "__main__":
+
+    def add(name: str, diagram: DiagramItem) -> None:
+        sys.stdout.write(f"<h1>{escapeHtml(name)}</h1>\n")
+        diagram.writeSvg(sys.stdout.write)
+        sys.stdout.write("\n")
+
+    sys.stdout.write("<!doctype html><title>Test</title><body>")
+    with open("test.py", "r", encoding="utf-8") as fh:
+        exec(fh.read())  # pylint: disable=exec-used
+    sys.stdout.write(
+        """
 		<style>
 		.blue text { fill: blue; }
 		</style>
-		''')
-	sys.stdout.write('</body></html>')
+		"""
+    )
+    sys.stdout.write("</body></html>")
diff --git a/railroad_diagrams.egg-info/PKG-INFO b/railroad_diagrams.egg-info/PKG-INFO
index 9c4738d..3df634d 100644
--- a/railroad_diagrams.egg-info/PKG-INFO
+++ b/railroad_diagrams.egg-info/PKG-INFO
@@ -1,177 +1,164 @@
 Metadata-Version: 2.1
 Name: railroad-diagrams
-Version: 1.1.1
+Version: 3.0.1
 Summary: Generate SVG railroad syntax diagrams, like on JSON.org.
 Home-page: https://github.com/tabatkins/railroad-diagrams
 Author: Tab Atkins
 Author-email: jackalmage@gmail.com
 License: UNKNOWN
-Description: Railroad-Diagram Generator
-        ==========================
-        
-        <a href="https://github.com/tabatkins/railroad-diagrams/blob/gh-pages/images/rr-title.svg"><img src="https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-title.svg?sanitize=true" alt="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" title="Diagram(Stack('Generate', 'some'), OneOrMore(NonTerminal('railroad diagrams'), Comment('and more')))" width=10000></a>
-        
-        This is a small library for generating railroad diagrams
-        (like what [JSON.org](http://json.org) uses)
-        using SVG, with both JS and Python ports.
-        
-        Railroad diagrams are a way of visually representing a grammar
-        in a form that is more readable than using regular expressions or BNF.
-        They can easily represent any context-free grammar, and some more powerful grammars.
-        There are several railroad-diagram generators out there, but none of them had the visual appeal I wanted, so I wrote my own.
-        
-        [Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
-        
-        (This is the README for the Python port;
-        to see the JS README, visit <https://github.com/tabatkins/railroad-diagrams>.)
-        
-        Diagrams
-        --------
-        
-        Constructing a diagram is a set of nested calls:
-        
-        ```python
-        from railroad import Diagram, Choice
-        d = Diagram("foo", Choice(0, "bar", "baz"))
-        d.writeSvg(sys.stdout.write)
-        ```
-        
-        A railroad diagram must be started as a `Diagram` object,
-        which takes a list of diagram items,
-        defined below.
-        
-        The `Diagram()` constructor also optionally takes some keyword arguments:
-        
-        * `css`: If passed, is the CSS you would like the diagram to include.
-            If you don't pass anything, it defaults to including `railroad.DEFAULT_STYLE`.
-            If you don't want it to include any css at all in the diagram
-            (perhaps because you're including the `railroad.css` file manually in your page, and don't need each diagram to duplicate the CSS in itself),
-            pass `css=None`.
-        * `type`: JSON.org, the inspiration for these diagram's styling, technically has two varieties of Diagrams: a "simple" kind it uses for "leaf" types like numbers, and a "complex" kind which is used for container types like arrays. The only difference is the shape of the start/end indicators of the diagram.
-        
-            Diagrams default to being "simple", but you can manually choose by passing `type="simple"` or `type="complex"`.
-        
-        After constructing a Diagram, you can call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
-        
-        To output the diagram, call `.writeSvg(cb)` on it, passing a function that'll get called repeatedly to produce the SVG markup. `sys.stdout.write` (or the `.write` property of any file object) is a great value to pass if you're directly outputting it; if you need it as a plain string, a `StringIO` can be used.
-        
-        If you need to walk the component tree of a diagram for some reason, `Diagram` has a `.walk(cb)` method as well, which will call your callback on every node in the diagram, in a "pre-order depth-first traversal" (the node first, then each child).
-        
-        Components
-        ----------
-        
-        Components are either leaves (containing only text or similar)
-        or containers (containing other components).
-        
-        The leaves:
-        * Terminal(text, href?, title?, cls?) or a bare string - represents literal text.
-        
-            All arguments past the first are optional:
-            * 'href' makes the text a hyperlink with the given URL
-            * 'title' adds an SVG `<title>` element to the element,
-                giving it "hover text"
-                and a description for screen-readers and other assistive tech
-            * 'cls' is additional classes to apply to the element,
-                beyond the default `'terminal'`
-        
-        * NonTerminal(text, href) - represents an instruction or another production.
-        
-            The optional arguments have the same meaning as for Terminal,
-            except that the default class is `'non-terminal'`.
-        
-        * Comment(text, href) - a comment.
-        
-            The optional arguments have the same meaning as for Terminal,
-            except that the default class is `'non-terminal'`.
-        
-        * Skip() - an empty line
-        
-        * Start(type, label) and End(type) - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a Start() explicitly (as the first child of the Diagram!). The "type" attribute takes either "simple" (the default) or "complex", a la Diagram() and ComplexDiagram(). All arguments are optional.
-        
-        The containers:
-        * Sequence(...children) - like simple concatenation in a regex.
-        
-            ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
-        
-        * Stack(...children) - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
-        
-            ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
-        
-        * OptionalSequence(...children) - a Sequence where every item is *individually* optional, but at least one item must be chosen
-        
-            ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
-        
-        * Choice(index, ...children) - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle (starting from 0 for the first child).
-        
-            ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
-        
-        * MultipleChoice(index, type, ...children) - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either "any" (1+ branches can be taken) or "all" (all branches must be taken).
-        
-            ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
-        
-        * HorizontalChoice(...children) - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
-        
-        	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
-        
-        * Optional(child, skip?) - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter is `True`, it instead puts the Skip() in the straight-line path, for when the "normal" behavior is to omit the item.
-        
-            ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
-        
-        * OneOrMore(child, repeat?) - like `+` in a regex.  The 'repeat' argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
-        
-            ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
-        
-        * AlternatingSequence(option1, option2) - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
-        
-            ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
-        
-        * ZeroOrMore(child, repeat?, skip?) - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
-        
-            ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
-        
-        * Group(child, label?) - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
-        
-            ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
-        
-        
-        Options
-        -------
-        
-        There are a few options you can tweak, living as UPPERCASE_CONSTANTS at the top of the module; these can be adjusted via `railroad.OPTION_NAME_HERE = "whatever"`.
-        Note that if you change the text sizes in the CSS,
-        you'll have to adjust the text metrics here as well.
-        
-        * VS - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
-        * AR - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
-        * DIAGRAM_CLASS - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
-        * STROKE_ODD_PIXEL_LENGTH - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
-        * INTERNAL_ALIGNMENT - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
-        * CHAR_WIDTH - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
-        * COMMENT_CHAR_WIDTH - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
-        * DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
-        
-        Caveats
-        -------
-        
-        SVG can't actually respond to the sizes of content; in particular, there's no way to make SVG adjust sizing/positioning based on the length of some text.  Instead, I guess at some font metrics, which mostly work as long as you're using a fairly standard monospace font.  This works pretty well, but long text inside of a construct might eventually overflow the construct.
-        
-        License
-        -------
-        
-        This document and all associated files in the github project are licensed under [CC0](http://creativecommons.org/publicdomain/zero/1.0/) ![](http://i.creativecommons.org/p/zero/1.0/80x15.png).
-        This means you can reuse, remix, or otherwise appropriate this project for your own use **without restriction**.
-        (The actual legal meaning can be found at the above link.)
-        Don't ask me for permission to use any part of this project, **just use it**.
-        I would appreciate attribution, but that is not required by the license.
-        
 Keywords: diagrams,syntax,grammar,railroad diagrams
 Platform: UNKNOWN
-Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
+Classifier: License :: OSI Approved :: MIT License
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Requires-Python: >=3.7
 Description-Content-Type: text/markdown
+License-File: LICENSE
+
+Railroad-Diagram Generator, Python Version
+==========================================
+
+This is a small library for generating railroad diagrams
+(like what [JSON.org](http://json.org) uses)
+using SVG, with both JS and Python ports.
+[Here's an online dingus for you to play with and get SVG code from!](https://tabatkins.github.io/railroad-diagrams/generator.html)
+
+(This is the README for the Python port;
+see the [main README](https://github.com/tabatkins/railroad-diagrams) for other ports,
+and for more non-Python-specific information.)
+
+Diagrams
+--------
+
+Constructing a diagram is a set of nested calls:
+
+```python
+from railroad import Diagram, Choice
+d = Diagram("foo", Choice(0, "bar", "baz"))
+d.writeSvg(sys.stdout.write)
+```
+
+A railroad diagram must be started as a `Diagram` object,
+which takes a list of diagram items,
+defined below.
+
+The `Diagram()` constructor also optionally takes some keyword arguments:
+
+* `css`: If passed, is the CSS you would like the diagram to include.
+    If you don't pass anything, it defaults to including `railroad.DEFAULT_STYLE`.
+    If you don't want it to include any css at all in the diagram
+    (perhaps because you're including the `railroad.css` file manually in your page, and don't need each diagram to duplicate the CSS in itself),
+    pass `css=None`.
+* `type`: JSON.org, the inspiration for these diagram's styling, technically has two varieties of Diagrams: a "simple" kind it uses for "leaf" types like numbers, and a "complex" kind which is used for container types like arrays. The only difference is the shape of the start/end indicators of the diagram.
+
+    Diagrams default to being "simple", but you can manually choose by passing `type="simple"` or `type="complex"`.
+
+After constructing a Diagram, you can call `.format(...padding)` on it, specifying 0-4 padding values (just like CSS) for some additional "breathing space" around the diagram (the paddings default to 20px).
+
+To output the diagram, call `.writeSvg(cb)` on it, passing a function that'll get called repeatedly to produce the SVG markup. `sys.stdout.write` (or the `.write` property of any file object) is a great value to pass if you're directly outputting it; if you need it as a plain string, a `StringIO` can be used.
+This method produces an SVG fragment appropriate to include directly in HTML.
+
+Alternately, you can call `.writeStandalone(cb, css?)`,
+which'll format the SVG as a standalone document
+rather than as an HTML fragment.
+If you don't pass any `css`,
+it'll automatically include the `DEFAULT_STYLE`;
+you can include your own CSS instead by passing it as a string
+(or an empty string to include no CSS at all).
+
+If you need to walk the component tree of a diagram for some reason, `Diagram` has a `.walk(cb)` method as well, which will call your callback on every node in the diagram, in a "pre-order depth-first traversal" (the node first, then each child).
+
+Components
+----------
+
+Components are either leaves (containing only text or similar)
+or containers (containing other components).
+
+The leaves:
+* Terminal(text, href?, title?, cls?) or a bare string - represents literal text.
+
+    All arguments past the first are optional:
+    * 'href' makes the text a hyperlink with the given URL
+    * 'title' adds an SVG `<title>` element to the element,
+        giving it "hover text"
+        and a description for screen-readers and other assistive tech
+    * 'cls' is additional classes to apply to the element,
+        beyond the default `'terminal'`
+
+* NonTerminal(text, href) - represents an instruction or another production.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'non-terminal'`.
+
+* Comment(text, href) - a comment.
+
+    The optional arguments have the same meaning as for Terminal,
+    except that the default class is `'non-terminal'`.
+
+* Skip() - an empty line
+
+* Start(type, label) and End(type) - the start/end shapes. These are supplied by default, but if you want to supply a label to the diagram, you can create a Start() explicitly (as the first child of the Diagram!). The "type" attribute takes either "simple" (the default) or "complex", a la Diagram() and ComplexDiagram(). All arguments are optional.
+
+The containers:
+* Sequence(...children) - like simple concatenation in a regex.
+
+    ![Sequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-sequence.svg?sanitize=true "Sequence('1', '2', '3')")
+
+* Stack(...children) - identical to a Sequence, but the items are stacked vertically rather than horizontally. Best used when a simple Sequence would be too wide; instead, you can break the items up into a Stack of Sequences of an appropriate width.
+
+    ![Stack('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-stack.svg?sanitize=true "Stack('1', '2', '3')")
+
+* OptionalSequence(...children) - a Sequence where every item is *individually* optional, but at least one item must be chosen
+
+    ![OptionalSequence('1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optionalsequence.svg?sanitize=true "OptionalSequence('1', '2', '3')")
+
+* Choice(index, ...children) - like `|` in a regex.  The index argument specifies which child is the "normal" choice and should go in the middle (starting from 0 for the first child).
+
+    ![Choice(1, '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-choice.svg?sanitize=true "Choice(1, '1', '2', '3')")
+
+* MultipleChoice(index, type, ...children) - like `||` or `&&` in a CSS grammar; it's similar to a Choice, but more than one branch can be taken.  The index argument specifies which child is the "normal" choice and should go in the middle, while the type argument must be either "any" (1+ branches can be taken) or "all" (all branches must be taken).
+
+    ![MultipleChoice(1, 'all', '1', '2', '3')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-multiplechoice.svg?sanitize=true "MultipleChoice(1, 'all', '1', '2', '3')")
+
+* HorizontalChoice(...children) - Identical to Choice, but the items are stacked horizontally rather than vertically. There's no "straight-line" choice, so it just takes a list of children. Best used when a simple Choice would be too tall; instead, you can break up the items into a HorizontalChoice of Choices of an appropriate height.
+
+	![HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-horizontalchoice.svg?sanitize=true "HorizontalChoice(Choice(2,'0','1','2','3','4'), Choice(2, '5', '6', '7', '8', '9'))")
+
+* Optional(child, skip?) - like `?` in a regex.  A shorthand for `Choice(1, Skip(), child)`.  If the optional `skip` parameter is `True`, it instead puts the Skip() in the straight-line path, for when the "normal" behavior is to omit the item.
+
+    ![Optional('foo'), Optional('bar', 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-optional.svg?sanitize=true "Optional('foo'), Optional('bar', 'skip')")
+
+* OneOrMore(child, repeat?) - like `+` in a regex.  The 'repeat' argument is optional, and specifies something that must go between the repetitions (usually a `Comment()`, but sometimes things like `","`, etc.)
+
+    ![OneOrMore('foo', Comment('bar'))](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-oneormore.svg?sanitize=true "OneOrMore('foo', Comment('bar'))")
+
+* AlternatingSequence(option1, option2) - similar to a OneOrMore, where you must alternate between the two choices, but allows you to start and end with either element. (OneOrMore requires you to start and end with the "child" node.)
+
+    ![AlternatingSequence('foo', 'bar')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-alternatingsequence.svg?sanitize=true "AlternatingSequence('foo', 'bar')")
+
+* ZeroOrMore(child, repeat?, skip?) - like `*` in a regex.  A shorthand for `Optional(OneOrMore(child, repeat), skip)`.  Both `repeat` (same as in `OneOrMore()`) and `skip` (same as in `Optional()`) are optional.
+
+    ![ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-zeroormore.svg?sanitize=true "ZeroOrMore('foo', Comment('bar')), ZeroOrMore('foo', Comment('bar'), 'skip')")
+
+* Group(child, label?) - highlights its child with a dashed outline, and optionally labels it. Passing a string as the label constructs a Comment, or you can build one yourself (to give an href or title).
+
+    ![Sequence("foo", Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), "label"), "bar",)](https://github.com/tabatkins/railroad-diagrams/raw/gh-pages/images/rr-group.svg?sanitize=true "Sequence('foo', Group(Choice(0, NonTerminal('option 1'), NonTerminal('or two')), 'label'), 'bar',)")
+
+
+Options
+-------
+
+There are a few options you can tweak, living as UPPERCASE_CONSTANTS at the top of the module; these can be adjusted via `railroad.OPTION_NAME_HERE = "whatever"`.
+Note that if you change the text sizes in the CSS,
+you'll have to adjust the text metrics here as well.
+
+* VS - sets the minimum amount of vertical separation between two items, in CSS px.  Note that the stroke width isn't counted when computing the separation; this shouldn't be relevant unless you have a very small separation or very large stroke width. Defaults to `8`.
+* AR - the radius of the arcs, in CSS px, used in the branching containers like Choice.  This has a relatively large effect on the size of non-trivial diagrams.  Both tight and loose values look good, depending on what you're going for. Defaults to `10`.
+* DIAGRAM_CLASS - the class set on the root `<svg>` element of each diagram, for use in the CSS stylesheet. Defaults to `"railroad-diagram"`.
+* STROKE_ODD_PIXEL_LENGTH - the default stylesheet uses odd pixel lengths for 'stroke'. Due to rasterization artifacts, they look best when the item has been translated half a pixel in both directions. If you change the styling to use a stroke with even pixel lengths, you'll want to set this variable to `False`.
+* INTERNAL_ALIGNMENT - when some branches of a container are narrower than others, this determines how they're aligned in the extra space.  Defaults to `"center"`, but can be set to `"left"` or `"right"`.
+* CHAR_WIDTH - the approximate width, in CSS px, of characters in normal text (`Terminal` and `NonTerminal`). Defaults to `8.5`.
+* COMMENT_CHAR_WIDTH - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
+* DEBUG - if `True`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `False`.
+
diff --git a/railroad_diagrams.egg-info/SOURCES.txt b/railroad_diagrams.egg-info/SOURCES.txt
index ee6a9e1..8aed124 100644
--- a/railroad_diagrams.egg-info/SOURCES.txt
+++ b/railroad_diagrams.egg-info/SOURCES.txt
@@ -1,8 +1,11 @@
+LICENSE
 MANIFEST.in
+README-js.md
 README-py.md
 README.md
 railroad.py
 semver.txt
+setup.cfg
 setup.py
 railroad_diagrams.egg-info/PKG-INFO
 railroad_diagrams.egg-info/SOURCES.txt
diff --git a/semver.txt b/semver.txt
index 8cfbc90..13d683c 100644
--- a/semver.txt
+++ b/semver.txt
@@ -1 +1 @@
-1.1.1
\ No newline at end of file
+3.0.1
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 8bfd5a1..d4487ac 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,65 @@
+[flake8]
+ignore = 
+	E203, # just fires a bunch on list ranges
+	E265, # don't care
+	E501, # line lengths are a fake idea
+	E722, # bare accept is fine
+	F401, # handling unused imports elsewhere
+	F405, # ditto
+	F811, # flake8 doesn't understand typing overloads
+	W503, # this 'error' is completely backward and will change eventually
+	N802,
+	N803,
+	N806,
+	N812,
+	N815,
+	N816,
+max-line-length = 300
+per-file-ignores = 
+	bikeshed/lint/__init__.py:F401
+	bikeshed/h/__init__.py:F401
+	bikeshed/markdown/__init__.py:F401
+	bikeshed/shorthands/__init__.py:F401
+	bikeshed/refs/__init__.py:F401
+	bikeshed/update/__init__.py:F401
+	bikeshed/wpt/__init__.py:F401
+	bikeshed/__init__.py:F401
+	bikeshed/config/__init__.py:F401
+	bikeshed/stringEnum/__init__.py:F401
+
+[pylint.MESSAGES CONTROL]
+disable = 
+	arguments-differ,
+	broad-except,
+	c-extension-no-member,             # seems to be weird false pos
+	consider-using-f-string,           # don't care
+	duplicate-code,                    # dont' care
+	fixme,
+	invalid-name,                      # SUPER don't care
+	keyword-arg-before-vararg,         # literally nonsensical
+	missing-class-docstring,
+	missing-function-docstring,
+	missing-module-docstring,
+	no-else-break,                     # i prefer this actually
+	no-else-continue,                  # ditto
+	no-else-raise,                     # ditto
+	no-else-return,                    # ditto
+	no-self-use,                       # fine to do sometimes
+	pointless-string-statement,        # fine as alt comment syntax
+	redefined-builtin,
+	superfluous-parens,                # don't care
+	too-few-public-methods,            # dumb
+	too-many-arguments,                # dumb
+	too-many-boolean-expressions,      # needed
+	too-many-branches,                 # needed
+	too-many-instance-attributes,      # dumb
+	too-many-lines,
+	too-many-locals,                   # dumb
+	too-many-nested-blocks,
+	too-many-return-statements,
+	too-many-statements,
+	use-dict-literal,                  # don't care
+
 [egg_info]
 tag_build = 
 tag_date = 0
diff --git a/setup.py b/setup.py
index c4107ba..6c1952c 100644
--- a/setup.py
+++ b/setup.py
@@ -19,13 +19,10 @@ setup(
     url='https://github.com/tabatkins/railroad-diagrams',
     keywords=['diagrams', 'syntax', 'grammar', 'railroad diagrams'],
     classifiers=[
-        'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication',
+        'License :: OSI Approved :: MIT License',
         'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
     ],
+    python_requires=">=3.7",
 )
\ No newline at end of file

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-3.0.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-3.0.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-3.0.1.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-1.1.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-1.1.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/railroad_diagrams-1.1.1.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details