Nordström, Ari. “Don't Touch My SGML.” Presented at Balisage: The Markup Conference 2026, Washington, DC, August 3 - 7, 2026. In Proceedings of Balisage: The Markup Conference 2026. Balisage Series on Markup Technologies, vol. 31 (2026). https://doi.org/10.4242/BalisageVol31.Nordstrom01.
Balisage: The Markup Conference 2026 August 3 - 7, 2026
Balisage Paper: Don't Touch My SGML
Ari Nordström
Ari is an independent markup geek based in Göteborg, Sweden. He has provided
angled brackets to many organisations and companies across a number of borders
over the years, some of which deliver the rule of law, help dairy farmers make a
living, and assist in servicing commercial aircraft. And others are just for
fun.
Ari is the proud owner and head projectionist of Western Sweden's last
functioning 35/70mm cinema, situated in his garage, which should explain why he
once wrote a paper on automating commercial cinemas using XML.
SGML used to be the future of documentation. It still is, because no matter how
much we push for XML, we still need SGML. Entire business models rely on it, and if
we want to use our newfound technologies (hey, XML is only 28 as I write this), we'd
better make sure we can include the old. Author in XML? Yes, sure. Publish the XML?
Yes, absolutely. But don't touch my SGML
This paper starts off from a use case involving three separate SGML vocabularies
that had to be supported going forwards, no matter what. That use case evolved into
a more general approach to converting SGML to XML, including adding other SGML
vocabularies when needed, to take advantage of modern XML technologies for authoring
and publishing, including publishing back to SGML when needed.
The resulting set of scripts and resources has been made freely available in the
hope that it can help others looking for a way to manage SGML.
A well-known software provider asked me to help implement the ATA iSpec 2200 aerospace
standard [ATA iSpec 2200] for their
client. I warned them that SGML is outdated and poorly supported by their own software—I
have plenty of experience trying to implement both SGML and XML for it[1]. They insisted their SGML support was fixed, so I accepted the contract,
though I recommended authoring in XML and only publishing to SGML.
Testing proved otherwise and I found numerous issues. Crucially, while the latest
build had some functionality, the client’s older version failed entirely.
Then I got to meet the client.
It quickly emerged that ATA is only one of the many SGML and XML markup vocabularies
they use. There is the SGML-based standard for technical manuals, MIL-STD-38784 [MIL-STD-38784], but also several
flavours of the aerospace standard S1000D [S1000D], some SGML (issues
1.8, and 2.3) and some XML (the 4.x range , of which issue 4.1 for authoring was a
must).
Note
The aerospace industry is very, very conservative. Keeping planes airborne is the
priority, making system upgrades a secondary concern to data consistency[2]. Aerospace manufacturing is largely a collaborative process, involving
hundreds or even thousands of organisations and, sometimes, multiple nations working
together[3] to produce aircraft. To collaborate, the partners need to agree to use
specific versions of the standards; the information is shared and reused by multiple
organisations.
Being a mere contractor, enlisted to help out with implementing SGML, I'll admit that
all this felt rather uncomfortable. SGML wouldn't work, but neither would proposing
to
move everything to XML. A middle ground was needed.
The Middle Ground
XML is the better choice today, of course. It's not because XML would somehow be
better than SGML, whatever that means[4], but it's important to note that there's a lot of support for XML nowadays,
be it tools, processes or related standards, but very, very little for SGML. XML may
not
be the hottest new thing anymore, but it's mature and literally all over the place.
SGML, on the other hand, is literally what XML replaced. The infrastructure, the
standardisation, the tools, it all happened for XML[5].
I've sold in XML in place of SGML for aerospace before, the idea always being to
convert the SGML to XML, author everything in XML, and use XML technologies to publish
to all required output formats, including SGML. For an ATA iSpec 2200 project I did
in
the past [Topic-based SGML? Really?], we had something like this:
Figure 1: ATA SGML to XML Conversion and Publishing
Of course, now I also needed to support MIL-STD-38784 and S1000D.
Looking closer at MIL-STD-38784, I found that the US Government had already done some
of my work for me. There was an XML version of the DTD from around 2005, so rather
than
spending time writing a new XML MIL-SPEC-like DTD, I could use
theirs:
Figure 2: MIL-STD-38784 SGML to XML
The S1000D Issue 1.8 SGML DTDs (6 of them) evolved into XML Schemas from issue 3.0
and
onwards, so I resolved to use issue 4.1 XML, now 26 XML Schemas and a host of new
features. Importantly, issue 4.1 includes a way to define business
rules (the BREX data module type) declaring how the
standard is to be used. In the case of converting 1.8 SGML to 4.1 XML, then, I could
define business rules that limit the use of 4.1 to features also available in
1.8.:
Figure 3: S1000D 1.8 SGML to 4.1 XML
Of course, the 4.1 XML would be published to any required format, including issue
1.8
SGML.
Rather than creating yet another one-off, I resolved to write a more generic library,
extensible to other SGML vocabularies, and make it freely available to anyone still
needing to convert SGML to XML. The results are on GitHub [SGML to XML TNG].
SGML to XML
Common to all three is that they all need to convert SGML to well-formed XML; we
always need to handle the following:
DTDs; we'll need to know about things like tag omission[6], empty elements, default attribute values, etc.
Note
Tag omission, as indicated in the DTD, might frequently leave out end
tags.
Entities (graphic, reusable text fragments, character sets, etc)
Syntax
There's no such thing as a well-formed SGML file; they must all reference a DTD[7] using a DOCTYPE declaration. The DOCTYPE will
usually have an internal subset that contains entity declarations for
graphics and reusable text, but it can also include part of, or the entire DTD.
This is important, because when we process SGML, we'll need to use James Clark's SP
package [SP - An SGML System Conforming to International Standard ISO 8879 -- Standard Generalized
Markup Language]—SP is
the only game in town, really, but includes everything we need, from parsing
(nsgmls) and normalisation (spam) to SGML to XML syntax
conversion (sx). SP is all about parsing and processing instances, however,
so we need to normalise the SGML—pull the DTD and everything else into a single
file—before we can convert it to XML syntax.
In addition, an SGML document will use an SGML declaration[8], a text file that configures the SGML as used by the instances. It'll
commonly define things like character sets to be used, attribute literal lengths,
and
whether or not we care about case for tag names, but there's a lot more. For example,
we
may redefine the tag limiters to be used.
That's pretty much where I drew the line; whilst case sensitivity (or the lack of
it)
is important, I don't intend to support curly-bracket tag limiters just yet. And there
are other features that give me pause.
SGML Processing
Let's assume we have this rather trivial SGML DTD:
The graphic element has two ways of referencing a graphic, an
ENTITY-based using @name and a direct link using
@href. Before you protest, remember that this DTD is just for
testing (it's part of my repo for that purpose—see SGML to XML TNG).
This is assumed to use Unicode, but most real-life SGML DTDs will reference
external ISO character entity sets since the DTDs were usually conceived before
Unicode. For example, ATA DTDs include something along these lines[9]:
<!ENTITY % ISOtech PUBLIC
"ISO 8879-1986//ENTITIES General Technical//EN" >
<!ENTITY % ISOpub PUBLIC
"ISO 8879-1986//ENTITIES Publishing//EN" >
<!ENTITY % ISOnum PUBLIC
"ISO 8879-1986//ENTITIES Numeric and Special Graphic//EN" >
<!ENTITY % ISOgrk1 PUBLIC
"ISO 8879-1986//ENTITIES Greek Letters//EN" >
<!ENTITY % ISOgrk2 PUBLIC
"ISO 8879-1986//ENTITIES Monotoniko Greek//EN" >
<!ENTITY % ISOgrk3 PUBLIC
"ISO 8879-1986//ENTITIES Greek Symbols//EN" >
<!ENTITY % ISOgrk4 PUBLIC
"ISO 8879-1986//ENTITIES Alternative Greek Symbols//EN" >
<!ENTITY % ISOlat1 PUBLIC
"ISO 8879-1986//ENTITIES Added Latin 1//EN" >
<!ENTITY % ISObox PUBLIC
"ISO 8879-1986//ENTITIES Box and Line Drawing//EN" >
<!ENTITY % ISOamso PUBLIC
"ISO 8879-1986//ENTITIES Added Math Symbols: Ordinary//EN" >
<!ENTITY % ISOamsc PUBLIC
"ISO 8879-1986//ENTITIES Added Math Symbols: Delimiters//EN" >
%ISOtech; %ISOpub; %ISOnum; %ISOgrk1; %ISOgrk2; %ISOgrk3;
%ISOgrk4; %ISOlat1; %ISObox; %ISOamso; %ISOamsc;
An entity like — would have a corresponding entity
declaration:
<!ENTITY mdash SDATA "[mdash ]"--=em dash-->
The SGML processing, all of which is in an Ant build file, involves the
following:
Preprocessing specific to the SGML at hand. When I need to do this, it's
usually about removing weirdly placed newlines that cause havoc later on if
left alone.
<!DOCTYPE doc [
<!NOTATION jpeg SYSTEM "">
<!ENTITY bill SYSTEM "/home/ari/Documents/projects/tng-sgml2xml/doc/data/Bill_Murray.jpg" NDATA jpeg>
]>
For Unicode content, this is fine, but anything with character entities
will have problems[11].
For anything NOT Unicode, we need to insert the missing entity
declarations and references to the internal subset, but rather than using
SGML-only SDATA entities, they need to reference the Unicode
characters:
<!ENTITY mdash "—"> <!-- EM DASH -->
Since they won't harm anything if not used, I choose to always insert a
standard set. We get this:
<!DOCTYPE doc [<!-- This maps SGML character entities to their Unicode equivalents -->
<!-- Based on DocBook 4.1.2 XML entities --><!-- ISO 8879 official entity sets -->
<!ENTITY % iso-amsa PUBLIC "ISO 8879:1986//ENTITIES Added Math Symbols: Arrow Relations//EN"
"/home/ari/Documents/repos/sgml2xml-tng/modules/common/schemas/sgml/xml-entities/iso-amsa.ent">
%iso-amsa;
<!ENTITY % iso-amsb PUBLIC "ISO 8879:1986//ENTITIES Added Math Symbols: Binary Operators//EN"
"/home/ari/Documents/repos/sgml2xml-tng/modules/common/schemas/sgml/xml-entities/iso-amsb.ent">
%iso-amsb;
<!– (etc) -->
<!ENTITY bill SYSTEM "/home/ari/Documents/projects/tng-sgml2xml/doc/data/Bill_Murray.jpg" NDATA jpeg>
]>
We should now have well-formed XML.
XML Processing
The well-formed XML needs to be converted to whatever the end vocabulary (DTD,
schema...) requires. Whilst the specifics will differ—certainly for the three
formats, above—the principle is the same: tweak the XML as necessary to produce
valid XML. This involves running a series of XSLT stylesheets, different for each
vocabulary, but the pipeline mechanism is the same.
A first step is usually to encode the XML in UTF-8 and get rid of the entity
declarations. Another is often to convert any entity-based graphics to direct
references; XML processing can do a lot, but entity queries and manipulation remain
an absolute pain[12].
The same Ant build file that converts the SGML to XML calls an XProc pipeline that
runs a manifest file listing the required XSLT stylesheets in the order in which
they run.
The sgml2xml-tng Repo
The SGML-to-XML conversion repository [SGML to XML TNG] is
organised like this:
Figure 4: SGML to XML TNG
modules/ contains the resources for each input SGML
vocabulary.
The modules here include ata/, s1000d/, and
milspec/. Each of these contain the SGML and XML DTDs (and
schemas), plus any pipelines, XSLT, and other resources required. There's
also an example module doc/ to provide a runnable
example.
And finally, common/ contains resources common to the
modules, including SDATA entities (SGML), Unicode entities
(XML), common XSLT, etc.
lib/ contains the SP package for all things SGML, plus the
MorganaXProc-IIIse XProc 3.x engine [MorganaXProc-III]
for XML conversions and the SchXslt 2 Schematron to XSLT
transpiler [SchXslt2] for Schematron validation. There's also the
XProc Batch library [XProc Batch] to
run pipelined XSLT transforms.
catalogs/ contains catalog files.
ant/ contains Ant macrodefs for the main Ant script.
Ant build files (build_sgml2xml.xml,
build_sgml2xml.properties.xml, and a local properties file
not shown here) to run the main conversion. The build file contains the main
logic.
Here's the idea:
build_sgml2xml.xml contains all the steps required to process
the SGML (see section “SGML Processing”) and the resulting (well-formed) XML (see
section “XML Processing”). As a reminder, we do
preprocess, normalise,
convert to XML syntax, insert character
entity declarations to the internal subset, encode
to UTF-8, and run the XML
pipeline.
The build is configured in an Ant local property file (a tweak of
build_sgml2xml.properties.xml, named
build_sgml2xml.properties.local.xml).
The SGML vocabularies are all modules. Each requires
source DTDs, target DTDs/schemas, and any processing, including XSLT and
XProc.
The repo [SGML to XML TNG] contains a rather trivial example, in the
doc/ folder.
If you want to add an SGML vocabulary (i.e., another module, check out the
included examples. You may be able to use the common entity sets, but then
again, you may not. The code makes the rather optimistic assumption that you
can, in fact, reuse what is there now but will not force you to do
so.
The initial well-formed XML is intended to go through a series of
XSLT-based steps, transforming the XML into some valid vocabulary. If this
is all that is needed, a new module will only require you to write the XSLT
steps and list them in an XML manifest file similar to the existing
ones.
If you need more, you can copy and edit, or fully replace, the default
XProc pipelines with your own to do whatever might be missing.
The step outputs can all be saved to help debugging:
doc/
├── data
│ └── (SGML and graphics input)
└── tmp
└── 20260311133633
├── debug
│ └── (XProc XSLT pipeline debug output)
├── out
│ └── (valid XML and graphics)
├── pre
│ └── (preprocessed SGML)
├── reports
│ └── (reports)
├── spam
│ └── (spam output)
├── sx
│ └── (sx output)
└── xml
└── (well-formed XML)
The Ant build also provides an optional nsgmls validation
step.
The XML processing was always going to be a series of XSLT transforms. I much
prefer the pipelined approach [Pipelined XSLT Transformations], and the default XML processing assumes a series of
XSLTs listed in a manifest.
The pipelines are XProc 3.0 run with Achim Berndzen's MorganaXProc-IIIse [MorganaXProc-III]. The
default pipelines use XProc Batch [XProc Batch], but this doesn't have to be the case.
There's also David Maus's SchXslt2 Schematron to XSLT 3.0 transpiler [SchXslt2] to run
Schematron validation where needed.
Modelling XML DTDs and Schemas
While XML is the modern standard, SGML’s modeling capabilities were, in some
respects, more robust. For example, SGML featured a logical AND group—allowing
elements to appear in any order but exactly once. In XML, achieving this level of
validation typically requires moving beyond DTDs to Schematron or XSD.
SGML also permitted exclusions and
inclusions—convenient shortcuts that often complicate conversion.
Exclusions allowed a designer to prohibit specific elements (like a table) from all
descendants of an element. Inclusions did the opposite, allowing a specific element
to appear anywhere within a hierarchy, bypassing standard model rules.
The inclusion mechanism is a particular challenge for XML DTDs. In ATA iSpec 2200, for example,
revision markers (REVST and REVEND) were inclusions. Because they could legally appear at different hierarchy levels,
they are impossible to model in an XML DTD without resorting to messy, repeatable
OR groups.
Two primary strategies exist for resolving these models during conversion:
Processing Instructions (PIs): Since PIs can occur
anywhere, they mimic SGML inclusions well. However, they require authoring
environments that can protect them from accidental deletion.
Structural Remodelling: This involves a significant
rewrite of content models, often replacing markers with attributes and
external validation logic. While more labour-intensive, remodelling is
usually the superior long-term solution as it avoids relying on PIs to carry
semantic meaning.
XML to SGML
Publishing from XML to SGML is akin to the SGML to XML conversion process, but in
reverse:
The DOCTYPE declaration is removed; we are moving from validating
to well-formed XML.
An SGML DOCTYPE is generated, including, for example, graphic
ENTITY declarations based on @href attributes,
NOTATION declarations, etc.
Various XML-only constructs are removed and any SGML-only content models are
generated. For example, we may want to remove the @href graphic
references and insert ENTITY-based references instead.
The XML is converted from UTF-8 to ISO character encoding.
The generated DOCTYPE declaration is inserted into the XML
document, replacing the XML declaration. This, by the way,
is where we leave XML-based processing behind and rely on Ant's text-based
capabilities and SP.
The SGML DOCTYPE is reinserted into the SGML, as it is not
retained by spam.
End Notes
Firstly, I confess to using AI (Gemini) to help generate the images. If you think
they
are ugly, then ping me and I'll silence you with my original art created in
draw.io. Just don't say I didn't warn you.
A couple of notes regarding sgml2xml-tng:
The -tng in the name means that this is the
next generation. The first was always meant only for a single set
of SGML DTDs, and it was very difficult to extend it to support any other
DTD.
There was a separate XML to SGML pipeline, equally difficult to extend.
The XML to SGML conversion is a tricky thing because there are so many ways to
produce valid SGML. Most of my code uses XML and XML technologies, which limits
what I can do, and then relies on Ant and James Clark's SP package to do
anything I can't. SGML empty elements were miserably complicated, yet they are
just one problem among many.
Which means that I drew the line at some of the more esoteric ways to produce
valid SGML. Curly rather than angled brackets will need to wait, I'm afraid[13].
A reviewer asked about any SP limitations or issues I encountered. The overall
answer would be not that many, but I'd certainly highlight its
difficulties in outputting SGML EMPTY elements (i.e., start tags
only) without me first using a trick to expand those while still in XML syntax.
I have not seen the kinds of ambiguous constructs the reviewer says SP knows
nothing about, but it's difficult to comment further without seeing examples[14].
You'll notice that the ATA and S1000D DTDs and XSDs are missing from the repo.
The ATA DTDs are proprietary and cost money to obtain, so I cannot legally
include them. For S1000D, you'll need to approve the terms of use before
downloading any DTD or schema.
Some will (rightly) ask why I use Ant. The answer nowadays is historical[15]; it's what I used for the first-generation implementations. I do
want to move everything to XProc 3.1, but it's going to be a huge job and one
that I currently hesitate to start[16], partly because of my current implementation relies on the free
version of MorganaXProc-III; XML Calabash wasn't ready when much of the initial
work was done.
I'd also like to thank some folks:
Henrik Mårtensson, who left the markup business long ago, but not before we
did a lot of SGML together
Sheila Thomson, who is awesome at writing Ant and who substantially helped
improve mine
Chris Pedoe, who not only believed me when I pointed out the built-in problems
in the software we had to work with, but who also encouraged me to go ahead with
my alternate solution
Nic Gibson, who not only convinced me of the virtues of pipelined XSLT, but
who also gave me the means to follow through
James Clark, who's been at the centre of everything I've done in SGML and XML;
for the longest time, I wanted him to sign my copy of SP
[1] Which, I guess, is why they contacted me in the first place.
[2] S1000D Change 1.8, for example, is more than 25 years old.
[3] Witness, for example, the Eurofighter Typhoon, a wholly European
collaboration to produce a multirole fighter aircraft. The Eurofighter is a
collaboration between several European nations and thousands of European
companies.
[4] In fact, I'd argue that it isn't. I love SGML and its flexibility, and I
really miss some of the constructs SGML allowed but XML does not.
[5] And keeps on happening. Just the other day, oXygen announced S1000D 4.x
support.
[6] SGML would allow you to, for example, leave out end tags if the parser
could infer its position.
[16] MorganaXProc-IIIse (standard edition) does not support
p:run—you'll need the extended edition for that. Yet my
current thinking is that I'd rely on p:run for quite a
lot.