Automated ingestion pipelines for biomedical and academic archives including PubMed Central (PMC), Europe PMC, and downstream indexing engines frequently reject submissions that pass standard Document Type Definition (DTD) checks. The architectural failure occurs at the boundary between structural grammar validation and semantic integrity enforcement.
Under ANSI/NISO Z39.96-2021 (JATS: Journal Article Tag Suite, Version 1.3), the declaration for display mathematics permits an open content model:
<!ELEMENT disp-formula (#PCDATA | %disp-formula-elements;)*>
Under this permissive DTD definition, a bare graphical equation block lacking contextual identifiers or textual equivalents is syntactically valid:
<disp-formula id="eq1">
<graphic xlink:href="eq1.jpg"/>
</disp-formula>
However, downstream automated publishing workflows and syndication aggregators enforce the JATS4R (JATS for Reuse) “Formulas (Math and Chemical)” recommendations via ISO Schematron (ISO/IEC 19757-3:2020). JATS4R mandates three strict operational constraints:
- Explicit Identification: Every
<disp-formula>element representing a numbered equation must contain an explicit child<label>element holding the sequence indicator (e.g.,<label>(1)</label>). - Structural Dual-Representation: Formula blocks must contain semantic MathML 3.0 (
<mml:math>), and when raster or vector image fallbacks are included, they must be wrapped within an<alternatives>container. - Accessibility Conformance: To comply with W3C WCAG 2.1 AA (Success Criterion 1.1.1 Non-text Content) and Section 508 accessibility mandates, every visual equation must possess an accessible programmatic equivalent via an
<alt-text>child node, a non-empty@altattribute on child<graphic>elements, or an embedded<mml:annotation encoding="application/x-tex">payload within the MathML tree.
When XML production vendors omit these structures, downstream ingestion engines trigger fatal validation faults, halting ingest queues and corrupting automated cross-reference (<xref>) resolution graphs.
Pipeline Architecture
The preflight architecture isolates DTD structural validation from semantic Schematron enforcement using a staged, headless verification model:
[Incoming JATS Payload]
│
▼
┌───────────────────────────┐
│ Stage 1: xmllint Parser │ ──(DTD Syntax Failure)──> [Exit Code 1 / Abort]
│ (ANSI/NISO Z39.96 DTD) │
└───────────────────────────┘
│ (Valid)
▼
┌───────────────────────────┐
│ Stage 2: Saxon-HE Engine │
│ (ISO Schematron Pipeline)│
└───────────────────────────┘
│
├──> [iso_dsdl_include.xsl] (Include resolution)
├──> [iso_abstract_expand.xsl] (Pattern instantiation)
└──> [iso_svrl_for_xslt2.xsl] (SVRL Generator)
│
▼
┌───────────────────────────┐
│ Stage 3: Compiled XSLT │ ──(Transforms Input XML)──> [validation-report.svrl]
└───────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Stage 4: SVRL Assertion Gate │
│ (XPath count failed-assert) │
└──────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
[failed-assert == 0] [failed-assert > 0]
(Exit Code 0) (Exit Code 1)
- Stage 1 (Syntactic Gate):
xmllintstreams the payload against the local JATS 1.3 DTD catalog to verify well-formedness, correct tag closure, and entity resolution without memory overhead. - Stage 2 (Compilation Gate): The Schematron rule set (
jats4r-formulas.sch) compiles into a single executable XSLT 2.0 stylesheet using the three-stage ISO Schematron skeleton processor (iso_dsdl_include.xsl,iso_abstract_expand.xsl,iso_svrl_for_xslt2.xsl). - Stage 3 (Semantic Execution): Saxon-HE executes the compiled stylesheet against the payload, projecting assertions into the Schematron Validation Report Language (SVRL, ISO/IEC 19757-3).
- Stage 4 (Assertion Gate): A command-line assertion evaluator parses the SVRL output for
<svrl:failed-assert>elements, extracting the contextual node IDs and failing the pipeline on non-zero counts.
Executable Implementation
Production ISO Schematron Rules (jats4r-formulas.sch)
The following Schematron schema validates formula labeling, fallback containment, and accessibility annotations:
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
queryBinding="xslt2">
<ns prefix="mml" uri="http://www.w3.org/1998/Math/MathML"/>
<ns prefix="xlink" uri="http://www.w3.org/1999/xlink"/>
<pattern id="jats4r-disp-formula-constraints">
<title>JATS4R Formula Enforcement</title>
<rule context="disp-formula">
<!-- Assertion 1: Mandatory Label Element -->
<assert test="exists(label)"
id="jats4r-err-formula-label-missing"
role="error">
[FATAL] Formula <value-of select="(@id, '[NO ID]')[1]"/>: Every <disp-formula> must contain an explicit <label> child element.
</assert>
<!-- Assertion 2: Label Cannot Be Nested Inside MathML -->
<report test=".//mml:math//label"
id="jats4r-err-formula-label-misplaced"
role="error">
[FATAL] Formula <value-of select="(@id, '[NO ID]')[1]"/>: <label> cannot be nested within <mml:math>. It must be an immediate child of <disp-formula>.
</report>
<!-- Assertion 3: Structured Alternatives or MathML Requirement -->
<assert test="exists(mml:math) or exists(alternatives/mml:math) or exists(alternatives/graphic)"
id="jats4r-err-formula-content-missing"
role="error">
[FATAL] Formula <value-of select="(@id, '[NO ID]')[1]"/>: Must contain <mml:math> or an <alternatives> wrapper containing MathML and graphical assets.
</assert>
<!-- Assertion 4: Graphic without Alternatives Wrapper Forbidden -->
<report test="exists(graphic) and not(parent::alternatives)"
id="jats4r-err-formula-naked-graphic"
role="error">
[FATAL] Formula <value-of select="(@id, '[NO ID]')[1]"/>: Standalone <graphic> found directly under <disp-formula>. Graphics must reside within an <alternatives> wrapper.
</report>
<!-- Assertion 5: Accessibility Fallback Verification -->
<assert test="exists(alt-text) or
exists(alternatives/graphic[@alt and normalize-space(@alt) != '']) or
exists(.//mml:annotation) or
exists(alternatives/mml:math//mml:annotation)"
id="jats4r-err-formula-accessibility-missing"
role="error">
[FATAL] Formula <value-of select="(@id, '[NO ID]')[1]"/>: Missing accessible alternative text. Provide an <alt-text> node, a populated @alt attribute on <graphic>, or a TeX <mml:annotation>.
</assert>
</rule>
</pattern>
</schema>
Canonical, Production-Compliant JATS 1.3 XML Segment
This markup satisfies both ANSI/NISO Z39.96-2021 DTD criteria and JATS4R accessibility and semantic rules:
<disp-formula id="formula-m1">
<label>(1)</label>
<alt-text>f of x equals the integral from negative infinity to positive infinity of F of k times e to the power of i k x d k</alt-text>
<alternatives>
<mml:math display="block">
<mml:semantics>
<mml:mrow>
<mml:mi>f</mml:mi>
<mml:mo stretchy="false">(</mml:mo>
<mml:mi>x</mml:mi>
<mml:mo stretchy="false">)</mml:mo>
<mml:mo>=</mml:mo>
<mml:msubsup>
<mml:mo>∫</mml:mo>
<mml:mrow>
<mml:mo>−</mml:mo>
<mml:mi>∞</mml:mi>
</mml:mrow>
<mml:mi>∞</mml:mi>
</mml:msubsup>
<mml:mi>F</mml:mi>
<mml:mo stretchy="false">(</mml:mo>
<mml:mi>k</mml:mi>
<mml:mo stretchy="false">)</mml:mo>
<mml:msup>
<mml:mi>e</mml:mi>
<mml:mrow>
<mml:mi>i</mml:mi>
<mml:mi>k</mml:mi>
<mml:mi>x</mml:mi>
</mml:mrow>
</mml:msup>
<mml:mi>d</mml:mi>
<mml:mi>k</mml:mi>
</mml:mrow>
<mml:mml:annotation encoding="application/x-tex">f(x) = \int_{-\infty}^{\infty} F(k) e^{ikx} dk</mml:annotation>
</mml:semantics>
</mml:math>
<graphic xlink:href="formula-m1.svg" mime-subtype="svg+xml" mimetype="image" alt="f(x) = \int_{-\infty}^{\infty} F(k) e^{ikx} dk"/>
</alternatives>
</disp-formula>
Automated Validation Pipeline Script (validate-jats-math.sh)
This shell automation handles DTD preflight, compiles the Schematron schema via the standard ISO XSLT skeleton pipeline, processes batch inputs, and parses the generated SVRL report:
#!/usr/bin/env bash
set -Eeuo pipefail
# Path Configurations
SAXON_JAR="${SAXON_JAR:-/usr/share/java/saxon-he-12.4.jar}"
ISO_SCH_DIR="${ISO_SCH_DIR:-/opt/iso-schematron-xslt2}"
DTD_CATALOG="${DTD_CATALOG:-/opt/jats/catalog.xml}"
SCHEMA_SRC="jats4r-formulas.sch"
BUILD_DIR="build/schematron"
SVRL_OUTPUT_DIR="build/reports"
mkdir -p "${BUILD_DIR}" "${SVRL_OUTPUT_DIR}"
if [[ ! -f "${SAXON_JAR}" ]]; then
echo "[-] Saxon-HE JAR missing at: ${SAXON_JAR}" >&2
exit 2
fi
echo "[+] Phase 1: Compiling ISO Schematron Schema to XSLT 2.0 Engine..."
# Step 1: DSDL Include processing
java -jar "${SAXON_JAR}" \
-s:"${SCHEMA_SRC}" \
-xsl:"${ISO_SCH_DIR}/iso_dsdl_include.xsl" \
-o:"${BUILD_DIR}/stage1_included.sch"
# Step 2: Abstract Pattern expansion
java -jar "${SAXON_JAR}" \
-s:"${BUILD_DIR}/stage1_included.sch" \
-xsl:"${ISO_SCH_DIR}/iso_abstract_expand.xsl" \
-o:"${BUILD_DIR}/stage2_expanded.sch"
# Step 3: SVRL XSLT generator compilation
java -jar "${SAXON_JAR}" \
-s:"${BUILD_DIR}/stage2_expanded.sch" \
-xsl:"${ISO_SCH_DIR}/iso_svrl_for_xslt2.xsl" \
-o:"${BUILD_DIR}/compiled_validator.xsl"
echo "[+] Compilation successful: ${BUILD_DIR}/compiled_validator.xsl"
# Process all XML files passed as arguments
FAILED_DOCUMENTS=0
for xml_file in "$@"; do
echo "[+] Validating: ${xml_file}"
base_name=$(basename "${xml_file}" .xml)
svrl_report="${SVRL_OUTPUT_DIR}/${base_name}.svrl"
# Fast-Fail Stage: DTD structural verification
if ! xmllint --catalogs --noout --dtdvalid "${DTD_CATALOG}" "${xml_file}" 2>/dev/null; then
echo "[-] [FAIL] DTD Validation failed for: ${xml_file}" >&2
FAILED_DOCUMENTS=$((FAILED_DOCUMENTS + 1))
continue
fi
# Semantic Stage: Run compiled Schematron XSLT via Saxon
java -jar "${SAXON_JAR}" \
-s:"${xml_file}" \
-xsl:"${BUILD_DIR}/compiled_validator.xsl" \
-o:"${svrl_report}"
# Verification Stage: Extract failed assertions from SVRL
failure_count=$(xmllint --xpath "count(//*[local-name()='failed-assert'])" "${svrl_report}")
if [[ "${failure_count}" -gt 0 ]]; then
echo "[-] [FAIL] ${failure_count} JATS4R violation(s) found in ${xml_file}:" >&2
xmllint --xpath "//*[local-name()='failed-assert']/*[local-name()='text']/text()" "${svrl_report}" >&2
echo "" >&2
FAILED_DOCUMENTS=$((FAILED_DOCUMENTS + 1))
else
echo "[+] [PASS] All formula constraints satisfied for ${xml_file}"
fi
done
if [[ "${FAILED_DOCUMENTS}" -gt 0 ]]; then
echo "[-] Execution terminated with ${FAILED_DOCUMENTS} failing document(s)." >&2
exit 1
fi
echo "[+] Ingestion batch successfully verified."
exit 0
Automated Validation
The verification command targets the SVRL output, utilizing xmllint XPath parsing to return a machine-actionable boolean exit state for continuous integration runners:
# Execute validation over target manuscript
./validate-jats-math.sh manuscripts/sample-article.xml
# Direct CLI single-file inspection returning exit code 0 if compliant, 1 if failures exist
xmllint --xpath "boolean(//*[local-name()='failed-assert'])" build/reports/sample-article.svrl | grep -q "false"
If any formula within sample-article.xml violates the Schematron rule, the assertion resolves to true, causing grep -q "false" to return an exit code of 1 and terminating the CI step.
Production Gotchas
- Misplaced
<label>Inside MathML Tokens: Automatic TeX-to-MathML conversion engines frequently map equation numbers into<mml:mtext>or<mml:mi>tokens located inside the<mml:math>hierarchy (e.g.,\tag{1}). Downstream JATS resolvers require the<label>to be a direct child of<disp-formula>. Labels inside MathML are invisible to citation indexers, causing broken internal hyperlinks (<xref ref-type="bibr">) in downstream HTML outputs. - Undeclared XML Namespaces in MathML Nodes: Stripping and re-inserting MathML via regex or naive string parsing often strips the explicit
xmlns:mml="[http://www.w3.org/1998/Math/MathML](http://www.w3.org/1998/Math/MathML)"namespace declaration. While single-pass DTD parsers with prefix maps may process the file, Saxon’s XSLT 2.0 schema runtime will fail element-matching silently or abort compilation, causing assertions againstmml:mathto miss all instances. - Empty or Whitespace-Only
@altAttributes: Typesetting platforms often inject an emptyalt=""attribute on<graphic>tags within<alternatives>to bypass basic presence checks. This circumvents basic attribute validation while failing accessibility engines. Schematron assertions must explicitly evaluatenormalize-space(@alt) != ''to guarantee usable fallback text.



