An XSD to html documentation generator
  • Python 73.4%
  • HTML 9.6%
  • JavaScript 9.1%
  • CSS 7.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-23 14:46:47 +00:00
.forgejo/workflows Use the actions mirrored on code.wompom.com 2026-10-22 10:27:14 +00:00
scripts EA-styled neighbourhood previews, collapsed index, pages for overview diagrams 2026-10-22 14:08:52 +00:00
src/prettyxsd Link concept names in IFOPT documentation and SIRI's older names (1.11.0) 2026-10-23 14:24:54 +00:00
tests Link concept names in IFOPT documentation and SIRI's older names (1.11.0) 2026-10-23 14:24:54 +00:00
.gitignore Fixed the sidebar to work better 2026-09-28 22:50:18 +02:00
.python-version Initial commit with full documentation generation 2026-09-28 19:42:24 +02:00
LICENSE Licence prettyxsd under the EUPL 1.2 instead of the GPL 2026-10-22 18:30:39 +00:00
PLAN.md Sidebar on Transmodel pages: package tree with an A-Z view 2026-10-22 14:09:04 +00:00
pyproject.toml Link concept names in IFOPT documentation and SIRI's older names (1.11.0) 2026-10-23 14:24:54 +00:00
README.md Link concept names in IFOPT documentation and SIRI's older names (1.11.0) 2026-10-23 14:24:54 +00:00
uv.lock Link concept names in IFOPT documentation and SIRI's older names (1.11.0) 2026-10-23 14:24:54 +00:00

prettyxsd

Generate a browsable, static HTML reference from XML Schema Definition (XSD) files. The output includes an element index, a searchable global-element sidebar, pages for global elements and named types, links between declarations, and local CSS/JavaScript assets. The generated site can be opened locally or hosted as a static site; it does not require a web server or network connection.

Requirements

  • Python 3.14 or newer
  • uv

Install and run

From this directory, sync the project and run the CLI with uv:

uv sync
uv run prettyxsd --input path/to/schema.xsd --output path/to/site

Use --input more than once to provide multiple schema entry points:

uv run prettyxsd \
  --input schemas/main.xsd \
  --input schemas/another-entry-point.xsd \
  --output docs/

Use --title to set the documentation title shown in the sidebar and browser tab (default prettyxsd), and --logo to replace the default mark at the top left with an image file, which is copied into the site's assets/ directory:

uv run prettyxsd --input schemas/main.xsd --output docs/ --title "NeTEx" --logo netex-logo.png

The command recursively resolves relative local xsd:include and xsd:import schema locations. Remote schema locations are intentionally not fetched. Missing local dependencies and remote locations are reported as notices in the terminal and generated site; generation continues with the schemas it could load.

Every schema file gets a highlighted source page under src/, at its path relative to the deepest directory holding all the loaded schemas. To link those source pages to their files on GitHub, pass the GitHub URL for the directory containing the first input XSD. For example, NeTEx NX.xsd is in xsd/, so use:

uv run prettyxsd \
  --input ../NeTEx/xsd/NX.xsd \
  --output docs/netex \
  --github-base-url https://github.com/TransmodelEcosystem/NeTEx/tree/v2.0/xsd

Each source page under that directory then has a "View on GitHub" link to the file's GitHub blob URL, which follows the line the page is showing. Everything else (element, type and index pages, and the trees on them) links to the site's own source pages, never to GitHub. When the schemas come from several repositories, give --github-base-url DIR=URL once per repository's local directory; a schema takes the URL of the most specific directory that holds it:

uv run prettyxsd \
  --variant siri=SIRI/xsd/siri.xsd --variant opra=OpRa/xsd/opra.xsd \
  --github-base-url SIRI=https://github.com/TransmodelEcosystem/SIRI/tree/v2.3 \
  --github-base-url OpRa=https://github.com/TransmodelEcosystem/OpRa/tree/1.0rc \
  --output docs

To link NeTEx declarations to the Transmodel 6.2 concepts they implement, pass --transmodel:

uv run prettyxsd --input ../NeTEx/xsd/NX.xsd --output docs/netex --transmodel

NeTEx names its elements after Transmodel concepts in CamelCase, so StopPlace is the STOP PLACE concept and TypeOfLine is TYPE OF LINE. With --transmodel, each element and type in the NeTEx, SIRI, OpRa or IFOPT (ifopt, acsb) namespace whose name matches a Transmodel class gets a "Transmodel concept" link under its heading to that class's page. Types match after their structure suffix is removed (StopPlace_VersionStructure, TypeOfLine_ValueStructure), view elements match the concept they view (StopPlaceView), and a few names that differ from Transmodel's are mapped by hand (Timeband is TIME BAND). Concept names written in capitals in their documentation are linked too, as in "a STOP PLACE" or "SITE PATH LINKs": the longest matching concept name wins, lower-case plural endings are read as the singular (ENTITies is ENTITY, PLACEs is PLACE), and concepts Transmodel has renamed are found by their other name, as are older names SIRI still uses (STOP POINT is SCHEDULED STOP POINT). Text in code and existing links is left alone. Use --transmodel-namespace (repeatable) to link declarations in other namespaces instead. The concept catalog is bundled with prettyxsd, so generation needs no network access; rebuild it from the published model with uv run python scripts/update_transmodel_catalog.py.

To publish the Transmodel model itself alongside the schemas, pass --transmodel-pages (it implies --transmodel). The site gets a transmodel/ section with an index of the model's packages, a page for each of its 1,633 classes and a page for each of its diagrams, and the Transmodel links on element and type pages, and in their documentation, go to these pages instead of transmodel-cen.eu. A class page shows the class's definition, attributes with their types, multiplicities and definitions, supertypes and subtypes, associations with their roles and multiplicities, the diagrams it appears on, the elements and types that implement it, in a table with one cell per standard (NeTEx, SIRI, OpRa), and a small preview of its neighbourhood that expands, on a click, into a drawing in which every class, attribute and relationship is a link. Diagram pages show EA's own image of the diagram from transmodel-cen.eu, with its clickable areas pointed at these pages; that includes the overview diagrams, such as those of the New Work Items packages, that show only other diagrams. The neighbourhood drawings, which EA has no picture of, are drawn in EA's style, with its class colours, stereotypes and square-cornered lines: aggregations have a diamond on the whole (filled for a composition) and associations an open arrow on each end they can be navigated towards, as in EA. On these pages the sidebar browses the model instead of the schema: its packages as a tree, opened at the current page, with each package's diagrams and concepts listed apart, or every concept and diagram A to Z, and a filter that searches names and packages. Everything is taken from the Enterprise Architect XMI export that transmodel-cen.eu publishes, and the drawings are made in advance, so generation needs neither Graphviz nor network access. Rebuild the model data and drawings with uv run python scripts/update_transmodel_model.py, which needs Graphviz's dot (or $DOT) and network access. The Transmodel pages are headed "Transmodel", with the logo given by --transmodel-logo IMAGE (default --logo). --transmodel-link TEXT=URL adds a link for further reading to its start page; repeat it for several.

To document several entry schemas that share most of their files, give each one with --variant NAME=XSD instead of --input. They are loaded together, so element, type, source and example pages are written once and shared, and each variant gets a start page with its own element tree at NAME/index.html (index.html lists the variants). --variant-title NAME=TITLE sets a variant's title (default --title). The sidebar, which has a variant switcher, lists only the declarations the reader's current variant loads, and links to pages outside it are muted. When an entry schema declares its own version of a name another variant also declares (NeTEx's timetable subset has its own PublicationDelivery), that variant gets its own pages for those names under NAME/pages/:

uv run prettyxsd \
  --variant nx=../NeTEx/xsd/NX.xsd --variant-title "nx=NeTEx 2.0" \
  --variant ifopt=../NeTEx/xsd/ifopt.xsd --variant-title "ifopt=IFOPT" \
  --output docs --redirects-caddyfile docs/redirects.caddy

To publish several standards on one site, put each standard's variants in a folder of their own, such as --variant netex/nx=... and --variant siri/siri=...; their start pages are then at netex/nx/index.html and siri/siri/index.html. A name has at most one folder, and one variant's name can't be another's folder. --variant-logo NAME=IMAGE gives a variant a logo of its own in place of --logo; the shared pages show the logo of the reader's current variant. A page's logo is also its favicon. The logo at the top left always leads to the site's index.html. --home-title TITLE adds a first breadcrumb that leads there too, and --folder-title FOLDER=TITLE a breadcrumb after it that leads to FOLDER/index.html, for pages in that folder's variants (on shared pages, the reader's current variant): for example Public transport docs / NeTEx / Elements / Blacklist. --home-logo IMAGE shows a small logo before the logo that leads to the site's index.html, and is every page's favicon in place of the logo; --home-logo-hover IMAGE gives it a second image of the same size that fades in over it on hover, and the logo then leads to its variant folder's index.html (on the Transmodel pages, to transmodel/index.html).

--site-url URL gives the address the site is published at, such as https://reference4pt.eu/; every page then carries Open Graph tags (title, site name, address) for link previews. --social-image IMAGE adds the picture those previews show, a PNG or JPEG (1200×630 suits most sites; SVG isn't shown).

A site built before pages were shared had each variant's pages under NAME/elements/.... --redirects-caddyfile FILE writes a Caddyfile snippet that permanently redirects those URLs to the shared pages; import it inside the site's block in the main Caddyfile. Use --site-path when the site isn't served from /.

Every declaration (elements, types, attributes, child elements/tree nodes, and enumeration values) shows its defining schema file and line number (e.g. netex_transportOrganisation_version.xsd:287), linked to that line on the file's source page.

To publish example instance documents alongside the reference, pass a directory of XML files with --examples. Every .xml file under it (recursively) gets a page under examples/ with its element names linked to their declarations, and each global element's page lists the examples that use it, each linked to the line of its first occurrence:

uv run prettyxsd --input ../NeTEx/xsd/NX.xsd --output docs/netex --examples ../NeTEx/examples

Elements are matched against the schema's content model, following the parent element's type, base types, groups and substitution groups, so a local element such as a Name inside a Line links to the row declaring it on the Line page. Elements the content model doesn't explain fall back to a global element of the same name. An example page stores the document as plain text plus a compact list of link targets per start tag; site.js highlights it and adds the links in the browser, which keeps the generated pages close to the size of the examples themselves.

Open docs/index.html in a browser to browse the generated documentation. The index tree is rooted in global elements declared by the supplied entry-point schema files; local structure is expanded and named model groups (xsd:group) are kept as labelled branches, while referenced global elements link to their detail pages. Declarations brought in only by includes/imports remain available in the reference list and sidebar without overwhelming the top-level tree. This supports organization schemas such as NeTEx NX.xsd, whose Nx element provides a useful browsing hierarchy.

What is documented

  • Global elements, with one page per element
  • Element documentation rendered as Markdown, including supported inline XSD markup
  • Child elements, occurrence ranges, referenced global elements, and types
  • Content inherited from base types, collapsed under one "inherited from" block per base type (oldest first) in element and type trees, with an "Expand inherited" toggle; NeTEx-style restrictions that restate their base types' groups are split back up by the type that declares each group, and attributes from base types are listed in a collapsed table naming the type each comes from
  • Named model groups, shown as group branches in the content model with the group's own documentation and occurrence range
  • Anonymous xsd:choice compositors with more than one alternative, shown as a choice branch so alternatives aren't misread as a required sequence
  • Anonymous xsd:sequence/xsd:all compositors that repeat, are optional, or carry their own documentation, shown as a labelled branch instead of being silently flattened away
  • Attributes, required/optional use, referenced types, documentation, and any default/fixed value
  • Elements with a default/fixed value, shown inline in the content-model tree
  • Named simple and complex types, with links from elements and child declarations
  • Simple type restrictions (minInclusive, maxInclusive, minExclusive, maxExclusive, minLength, maxLength, length, totalDigits, fractionDigits, whiteSpace, pattern), listed alongside the base type they narrow
  • Abstract elements and types, flagged with an abstract badge wherever they appear (their own page, substitution-group listings, content-model trees, and the global element list) since they cannot be used directly in an instance document
  • Known subtypes of a complex or simple type ("Derived types"), linked from the base type's page
  • xsd:list simple types, showing the linked item type ("Item type")
  • xsd:union simple types, showing each linked member type ("Member types"); inline anonymous member types are described in place (e.g. their restriction base and enumeration values) since they have no page of their own
  • The full substitution-group ancestry chain for an element ("Substitution group chain"), not just its immediate parent, so a deeply nested element like NeTEx's Operator shows its whole path back to the ultimate abstract root (Operator → TransportOrganisation_Dummy → Organisation_Dummy → DataManagedObject → EntityInVersion)
  • The full set of concrete elements reachable transitively through an element's substitution-group descendants ("Concrete substitution alternatives"), so an abstract root element's page lists every element that can actually be used in its place in an instance document, not just its immediate substitution members
  • Identity constraints (xsd:key, xsd:keyref, xsd:unique) declared directly on an element, listed in an "Identity constraints" table with their selector/field XPath expressions and documentation; a keyref's refer is resolved and linked to the key/unique it targets (which may be on the same or a different element's page), falling back to plain text when the target can't be resolved

Generated detail pages are grouped under elements/<namespace>/<name>.html and types/<namespace>/<name>.html. The path segments are readable slugs; a short hash is added only when distinct names would otherwise collide. Shared assets remain under assets/, and nested pages use relative links so the site works locally and when hosted under a subdirectory.

XSD documentation is rendered as Markdown, with supported inline XSD markup preserved. The generated HTML is then passed through a conservative allowlist sanitizer: unsafe elements are not emitted as HTML, and unsafe link schemes are removed. Legacy inline option markers such as for:- …, or- … are formatted as Markdown lists.

Development

Dependencies and lockfile are managed with uv (do not use pip):

uv sync
uv run python -m unittest discover -s tests -v

See PLAN.md for the implementation scope and design decisions.

Licence

Copyright Giles Antonio Radford (https://wompom.com/). prettyxsd is free software under the European Union Public Licence, version 1.2 (EUPL-1.2); see LICENSE. The code was written with AI assistance. The licence covers prettyxsd itself, not the schemas you document with it or the content of the sites it generates, which keep their own terms.