Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
479415b
docs: record the ISO constraints the toolchain has to meet
aaronmbauman Aug 31, 2026
d47e7a5
spec: add Metanorma ISO document skeleton
aaronmbauman Aug 31, 2026
62189ca
spec: add the OGF edition
aaronmbauman Aug 31, 2026
613e8b8
build: add Makefile with pinned Metanorma version
aaronmbauman Aug 31, 2026
f3fdcf1
build: add lint and check targets
aaronmbauman Aug 31, 2026
e16a2a6
ci: add a workflow to build the spec and attach the artifacts
aaronmbauman Aug 31, 2026
5045459
ci: add lint and fidelity check job
aaronmbauman Aug 31, 2026
a79fb8f
tools: add docx-to-adoc converter reading word/document.xml
aaronmbauman Aug 31, 2026
acaf40e
tools: map Word paragraph styles to AsciiDoc constructs
aaronmbauman Aug 31, 2026
2e69e01
tools: convert bookmarks and REF fields to AsciiDoc anchors
aaronmbauman Aug 31, 2026
022ecc0
tools: convert tables to AsciiDoc table syntax
aaronmbauman Aug 31, 2026
77db857
tools: convert footnotes and extract embedded images
aaronmbauman Aug 31, 2026
49f1e1b
tools: extract normalised text inventory from docx in reading order
aaronmbauman Aug 31, 2026
222ea6a
tools: add fidelity checker diffing Word against built output
aaronmbauman Aug 31, 2026
f5886a6
tools: scope the check to clauses present in the build
aaronmbauman Aug 31, 2026
f136659
spec: move front matter boilerplate to document metadata
aaronmbauman Aug 31, 2026
bce4eb6
spec: convert change history, authors, scope and terms
aaronmbauman Aug 31, 2026
49e0bf4
spec: convert Clauses 1-5 (Introduction, Infoset, Schema Model)
aaronmbauman Aug 31, 2026
5a91f3b
spec: resolve cross-references to Clauses 1-5
aaronmbauman Aug 31, 2026
75484b4
spec: convert Clauses 6-8 (Syntax Basics, Annotations, Scoping)
aaronmbauman Aug 31, 2026
bf25c62
spec: resolve cross-references to Clauses 6-8
aaronmbauman Aug 31, 2026
d36d8c2
spec: convert Clauses 9-11 (Processing, Representation Properties)
aaronmbauman Aug 31, 2026
3042290
spec: resolve cross-references to Clauses 9-11
aaronmbauman Aug 31, 2026
a02df3e
spec: convert Clause 12 (Framing)
aaronmbauman Aug 31, 2026
4065c86
spec: resolve cross-references to Clause 12
aaronmbauman Aug 31, 2026
e36e922
spec: convert Clause 13 (Simple Types)
aaronmbauman Aug 31, 2026
305b7bd
spec: resolve cross-references to Clause 13
aaronmbauman Aug 31, 2026
bdeb3ff
spec: convert Clauses 14-17 (Sequences, Choices, Arrays, Calculated)
aaronmbauman Aug 31, 2026
ed2904e
spec: resolve cross-references to Clauses 14-17
aaronmbauman Aug 31, 2026
3c5e6c0
spec: convert Clauses 18-19 (Expression Language, Regular Expressions)
aaronmbauman Aug 31, 2026
4009cae
spec: resolve cross-references to Clauses 18-19
aaronmbauman Aug 31, 2026
0267410
spec: convert Clauses 20-29 (Conformance, References)
aaronmbauman Aug 31, 2026
35ee61f
spec: resolve cross-references to Clauses 20-29
aaronmbauman Aug 31, 2026
6116c65
spec: convert Annexes A-G
aaronmbauman Aug 31, 2026
c0a9805
spec: resolve cross-references to Annexes A-G
aaronmbauman Aug 31, 2026
eed1e43
tools: generate property index from semantic XML
aaronmbauman Aug 31, 2026
7703848
tools: add cross-reference link checker
aaronmbauman Aug 31, 2026
c6bbd4a
ci: fail the build on unresolved cross-references
aaronmbauman Aug 31, 2026
55284b6
build: produce OGF and ISO variants from one body
aaronmbauman Aug 31, 2026
67ccd11
build: add release target writing versioned artifacts to docs/releases
aaronmbauman Aug 31, 2026
50a9f59
docs: document the build, its outputs and known toolchain issues
aaronmbauman Aug 31, 2026
1ade248
spec: anchor clause and property targets for cross-referencing
aaronmbauman Aug 31, 2026
b62985c
spec: link cross-references written as plain text
aaronmbauman Aug 31, 2026
ea5ba56
tools: verify structural invariants in the rendered document
aaronmbauman Aug 31, 2026
1d3e3f1
build: run the conversion check separately from make check
aaronmbauman Sep 1, 2026
cc07aba
docs: describe how to edit the specification
aaronmbauman Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/spec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: spec

# Builds the DFDL specification and runs the checks. Everything that renders
# the spec happens inside the pinned Metanorma image the Makefile names, so
# the runner installs nothing beyond ruff and black.
#
# Run it from the Actions tab. There is deliberately no push or pull_request
# trigger: whether this repository runs CI automatically, and on what, is a
# working group decision rather than something this workflow assumes. To make
# it automatic, add the triggers you want:
#
# on:
# workflow_dispatch:
# pull_request:
# push:
# branches: [master]

on:
workflow_dispatch:

# Read-only: this workflow never writes to the repository, publishes nothing,
# and uses no secrets, so it behaves identically for pull requests from forks.
permissions:
contents: read

# A newer run for the same ref supersedes an older one.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# Kept in step with IMAGE in the Makefile.
METANORMA_IMAGE: metanorma/metanorma:alpine-1.17.0

jobs:
build:
name: build (both editions)
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out the repository
uses: actions/checkout@v4

- name: Pull the pinned Metanorma image
run: docker pull "$METANORMA_IMAGE"

# Both editions, because a change can break one and not the other.
- name: Build the specification
run: make editions

- name: List the build output
run: ls -lR build

# The artifacts are how a reviewer reads the rendered spec without
# installing Docker, Metanorma or anything else.
- name: Upload the rendered specification
uses: actions/upload-artifact@v4
with:
name: dfdl-spec
path: |
build/iso/dfdl.pdf
build/iso/dfdl.html
build/ogf/dfdl.pdf
build/ogf/dfdl.html
if-no-files-found: error

check:
name: check (lint + validators)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out the repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
# Matches target-version in pyproject.toml.
python-version: '3.14'

- name: Install the linters
run: python -m pip install --disable-pip-version-check ruff==0.16.5 black==26.5.1

- name: Pull the pinned Metanorma image
run: docker pull "$METANORMA_IMAGE"

# `make check` runs ruff and black over tools/, builds the document,
# then runs each validator in tools/ against it. Any violation exits
# non-zero and fails this job.
- name: Lint and check fidelity
run: make check
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/_site/
/_site/
build/
relaton/
__pycache__/
123 changes: 123 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# DFDL specification build.
#
# Metanorma runs inside a pinned Docker image, so nothing needs to be
# installed locally and every build is reproducible.
#
# make xml semantic XML only (fast)
# make html HTML
# make pdf ISO-formatted PDF (slow)
# make all pdf + html
# make editions `all` for every edition, from one source state
# make lint ruff + black over tools/ (host tools, not the container)
# make check lint, then the validators in tools/ against the semantic XML
# make clean remove build output
#
# make release VERSION=x.y.z
# every edition, rendered from scratch into docs/releases/,
# which is committed; build/ is not.
#
# EDITION selects which edition to build; output lands in build/$(EDITION)/.
# Both editions render identically-named artifacts, so they need separate
# directories.
#
# make EDITION=ogf pdf
#
# ISO's house font (Cambria) is proprietary; --continue-without-fonts lets
# the build fall back to Noto Sans instead of prompting for a licence.
#
# The build also writes a relaton/ bibliography cache at the repo root; it is
# gitignored and kept between builds so references resolve offline.

IMAGE := metanorma/metanorma:alpine-1.17.0
STEM := dfdl

EDITIONS := iso ogf
EDITION ?= iso
SPEC_iso := spec/dfdl.adoc
SPEC_ogf := spec/dfdl-ogf.adoc
SPEC := $(SPEC_$(EDITION))
BUILD := build/$(EDITION)

# Metanorma names its output after the input file; the artifacts are
# renamed to $(STEM) so both editions land under the same names.
SRCSTEM := $(basename $(notdir $(SPEC)))

DOCKER := docker run --rm -v "$(CURDIR):/metanorma" $(IMAGE)
MN_FLAGS := --no-install-fonts --continue-without-fonts

RELEASE := docs/releases/$(VERSION)

.PHONY: all editions release xml html pdf lint check check-conversion clean

# One pdf pass also emits the HTML and the XML, so `all` is just `pdf`.
all: pdf

# Every edition in one invocation, so both come from the same source state.
editions:
@for e in $(EDITIONS); do \
echo "==> $$e"; \
$(MAKE) --no-print-directory EDITION=$$e all || exit 1; \
done

xml: FORMATS := xml
html: FORMATS := xml,html
pdf: FORMATS := xml,html,pdf

# Metanorma writes its output next to the input file, so move the artifacts
# into $(BUILD)/ afterwards.
xml html pdf:
@mkdir -p $(BUILD)
$(DOCKER) metanorma compile -t iso -x $(FORMATS) $(MN_FLAGS) $(SPEC)
@for f in $(dir $(SPEC))$(SRCSTEM).*; do \
case "$$f" in *.adoc) continue ;; esac; \
mv -f "$$f" "$(BUILD)/$(STEM)$${f#$(dir $(SPEC))$(SRCSTEM)}"; \
done
@echo "Output in $(BUILD)/"

# A release is committed, so it is rendered from scratch rather than
# published from whatever happens to be left in build/. The edition is part
# of the filename because build/ keeps them apart by directory instead.
release:
@test -n "$(VERSION)" || { echo "usage: make release VERSION=x.y.z" >&2; exit 1; }
$(MAKE) --no-print-directory clean
$(MAKE) --no-print-directory editions
@mkdir -p $(RELEASE)
@for e in $(EDITIONS); do \
for x in pdf html xml; do \
cp build/$$e/$(STEM).$$x $(RELEASE)/$(STEM)-$$e.$$x || exit 1; \
done; \
done
@echo "Release in $(RELEASE)/"

# ruff and black run on the host, not in the Metanorma container.
lint:
@if ls tools/*.py >/dev/null 2>&1; then \
ruff check tools/ && black --check tools/; \
else \
echo "No Python in tools/ yet; nothing to lint."; \
fi

# Runs every executable in tools/, plus any tools/check*.py, over the
# build. Tolerant of tools/ being empty or absent.
check: lint html
@checkers=$$({ find tools -maxdepth 1 -type f -perm -u+x; \
ls tools/check*.py; } 2>/dev/null | sort -u); \
if [ -z "$$checkers" ]; then \
echo "No validators in tools/ yet; nothing to check."; \
else \
for c in $$checkers; do \
echo "==> $$c"; \
if [ -x "$$c" ]; then "$$c" $(BUILD)/$(STEM).xml; \
else python3 "$$c" $(BUILD)/$(STEM).xml; fi || exit 1; \
done; \
fi

# Compares the build against the Word source, character for character. This
# proves the conversion; it is not part of `make check`, because a deliberate
# change to the specification is supposed to differ from GFD.240 and would
# fail it correctly. See spec/BUILD.md.
check-conversion: xml
python3 tools/fidelity-check.py $(BUILD)/$(STEM).xml

clean:
rm -rf build .ruff_cache
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ This working group maintains this standard and proposed extensions/additions to
* [DFDL Workgroup News](https://github.com/OpenGridForum/DFDL/wiki/DFDL-Workgroup-News)
* [DFDL Workgroup Calls/Meeting Minutes](https://github.com/OpenGridForum/DFDL/tree/master/calls)
* [DFDL Workgroup Current Documents](https://github.com/OpenGridForum/DFDL/tree/master/docs/current)
* [Building the specification](spec/BUILD.md)
* [Editing the specification](spec/EDITING.md)
* [OGF DFDL Homepage](http://www.ogf.org/dfdl)
* [Wikipedia DFDL](https://en.wikipedia.org/wiki/Data_Format_Description_Language)

Expand Down
Empty file added docs/releases/.keep
Empty file.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Config only for the helper scripts in tools/. This is not a Python package.

[tool.ruff]
target-version = "py314"
line-length = 88
116 changes: 116 additions & 0 deletions spec/BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Building the specification

Everything is driven from the `Makefile` at the repository root. `TOOLCHAIN.md`
records the ISO constraints the toolchain has to meet; this records how to run
it.

## What you need

Docker, and nothing else, to render the specification: Metanorma runs inside a
container. `make lint` and `make check` additionally use `ruff`, `black` and
Python 3 on the host, because the validators in `tools/` are not part of the
Metanorma image.

## Why the image is pinned

`IMAGE := metanorma/metanorma:alpine-1.17.0`

Metanorma bundles the ISO stylesheets, the XSL-FO renderer and the Relaton
bibliography stack, and their output moves between releases: page breaks shift,
generated numbering changes, and new style warnings appear. Pinning the tag
means a contributor, a reviewer and CI all render the same bytes from the same
source. The same tag is repeated in `.github/workflows/spec.yml`; change both
together.

## Targets

| Target | What it does |
| --- | --- |
| `make xml` | semantic XML only — the fast path, and what the validators read |
| `make html` | XML + HTML |
| `make pdf` | XML + HTML + PDF (slow) |
| `make all` | the same as `make pdf` — one pass emits all three |
| `make editions` | `make all` for every edition |
| `make lint` | `ruff` and `black` over `tools/` |
| `make check` | `lint`, then every validator in `tools/` against the semantic XML |
| `make release VERSION=x.y.z` | versioned deliverables for both editions |
| `make clean` | remove the build output |

`EDITION` selects a single edition, for example `make EDITION=ogf pdf`. It
defaults to `iso`.

## The validators

`make check` runs everything in `tools/` against the build.

| | |
|---|---|
| `link-check.py` | every cross-reference and citation resolves to exactly one target |
| `structure-check.py` | the rendered page says what it means: no sentence naming the wrong figure, no caption stranded, no markup leaking, nothing published that the Word source hides |
| `fidelity-check.py` | the converted text matches the Word source, character for character |

The first two answer questions that stay live for as long as the document is
edited: an erratum can strand a caption or break a reference, and neither shows
up in a diff a reviewer would notice.

`fidelity-check.py` is not one of them, and is not run by `make check`. It
compares the build against the Word source character for character, which proves
the conversion but fails on any deliberate change to the specification: an
erratum is *supposed* to differ from GFD.240. Run it on its own:

make check-conversion

Never teach it to ignore differences. A fidelity check that has been relaxed is
worse than none, because it still reports PASS.

It keeps a second use. "What has changed since GFD.240?" is a question an ISO
submission has to answer, and this answers it clause by clause.

## The two editions

The specification is published twice: as ISO/IEC 23415 and as OGF GFD.240. The
normative text is identical, so `spec/body.adoc` holds all of it and is included
by both masters — `spec/dfdl.adoc` and `spec/dfdl-ogf.adoc` — which differ only
in document metadata and the front matter they pull in. Editing the body edits
both editions.

Metanorma names its output after its input, and the Makefile renames the
artifacts to a common stem, so the two editions produce identically named files.
They are written to `build/iso/` and `build/ogf/` so neither can silently
overwrite the other. `make editions` renders both in one invocation, which also
guarantees they came from the same source state.

Each build directory holds `dfdl.xml` (semantic XML), `dfdl.presentation.xml`,
`dfdl.html`, `dfdl.pdf`, and the diagnostics: `dfdl.err.html` lists Metanorma's
style and structure warnings and is worth reading when output looks wrong.

## Releases

`build/` is gitignored, because a rendered PDF in every commit would bury the
source changes. Versioned deliverables are committed instead, under
`docs/releases/<VERSION>/`, the same way `docs/current/` holds the Word-era
`.docx`, `.pdf` and `.htm`.

make release VERSION=1.2.3

The target refuses to run without `VERSION`, cleans, builds both editions from
scratch rather than publishing whatever is left in `build/`, and copies the
deliverables out under names that carry the edition:

docs/releases/1.2.3/dfdl-iso.pdf dfdl-iso.html dfdl-iso.xml
docs/releases/1.2.3/dfdl-ogf.pdf dfdl-ogf.html dfdl-ogf.xml

## Known toolchain issues

**Cambria is proprietary.** ISO's house font cannot be redistributed, so the
build passes `--no-install-fonts --continue-without-fonts` and falls back to
Noto Sans. Font warnings in the log are expected, and line and page breaks in
the local PDF will not match ISO's own typesetting.

**Revision marks need the macro form.** The AsciiDoc role syntax `[.add]#text#`
is silently ignored — no error, no mark, the text just renders plain. Only
`add:[text]` and `del:[text]` produce revision marks.

**The bibliography cache is kept.** Builds write a `relaton/` cache at the
repository root. It is gitignored and reused, so references resolve without
network access after the first build; delete it if a reference looks stale.
Loading