General Applications
By General Applications6 min read

The S1000D Transform Loop: One Model, Two Component Trees

Software ArchitectureFrontend Engineering

A common architectural trap when modernizing S1000D technical publications is starting the debate in the wrong place: should the new Interactive Electronic Technical Manual (IETM) be built in React or Angular? Teams often focus on frontend performance while building the same flaw into their prototypes—attempting to parse and render complex S1000D XML schemas directly inside UI components.

The result is often a fragile, sluggish web application where schema parsing, cross-module reference resolution, and applicability logic are scattered across dozens of UI widgets. When performance tanks on a ten-thousand-page maintenance manual, teams blame the frontend framework. In reality, the failure occurred at the architectural boundary.

The correct architecture decouples the specification from the delivery target using an intermediate canonical publication model. Once that model is resolved, rendering is handled by a single recursive transform loop. Here is how that loop is structured, how it handles directed reference graphs and runtime applicability, and how it mounts cleanly into React, Angular, or downstream AI workflows.

The Seam: What the Loop Actually Walks

The transformation loop never touches raw XML. By the time rendering begins, data modules have already been decomposed and normalized into a generic node tree: `{ type, attrs, children, resolvedRef }`.

In the S1000D specification, descriptive narratives, procedural tasks, fault isolation trees, and illustrated parts catalogs share almost no structural markup in common. A procedure contains strict sequential steps, preliminary requirements, and safety warnings. A parts catalog contains hotspot coordinate bindings and part tables. Normalizing every content type into a common node primitive allows a single transformation engine to walk the entire publication corpus.

publicationSchema.ts
type PublicationNode = {
type: string; // "step" | "warning" | "hotspotFigure" | "partsListItem" | ...
attrs: Record<string, string>;
children: PublicationNode[];
// Populated during graph resolution, not parse time:
resolvedRef?: { dmc: string; node: PublicationNode } | null;
};

Resolving the Reference Graph

S1000D content is a graph, not a tree. A shared data module or reusable information object can live once in the Common Source Data Base (CSDB) and serve many maintenance procedures. In practice, one node can have many incoming edges.

Real publications also contain cycles. If Procedure A references Module B and Module B points back to Procedure A, a naive walker exhausts the browser stack. The resolver needs visited keys to stop the current path and a Data Module Code (DMC)-keyed cache to share repeated references. The example below does both.

resolveReferences.ts
function resolveReferences(
node: PublicationNode,
lookup: (dmc: string) => PublicationNode | undefined,
memo: Map<string, PublicationNode> = new Map(),
seen: Set<string> = new Set(),
): PublicationNode {
if (node.type === "dmRef") {
const dmc = node.attrs.dmc;
if (seen.has(dmc)) {
// Cycle detected: stop recursive resolution.
// The renderer can preserve the original reference as navigation.
return { ...node, resolvedRef: null };
}
const cached = memo.get(dmc);
if (cached) return { ...node, resolvedRef: { dmc, node: cached } };

const target = lookup(dmc);
if (!target) return { ...node, resolvedRef: null };
const resolvedTarget = resolveReferences(
target,
lookup,
memo,
new Set([...seen, dmc]),
);
memo.set(dmc, resolvedTarget);

return {
...node,
resolvedRef: {
dmc,
node: resolvedTarget,
},
};
}
return {
...node,
children: node.children.map((child) =>
resolveReferences(child, lookup, memo, seen),
),
};
}

Cache by DMC, not object reference. The same warning parsed twice has two object identities, but its DMC identifies one underlying module. The cache therefore returns one resolved record wherever it is referenced.

Keep cached modules configuration-neutral. If applicability mutates the resolved tree, include the active configuration in the cache identity; otherwise one variant can contaminate another.

A reference to a figure, table, step, or other internal element follows the same pattern: resolve its target module first, then use an ID-scoped lookup inside that module. A DMC cache alone is not enough.

Where Applicability Gets Applied

Applicability rules filter content by tail number, system variant, or equipment serial. Evaluate them at build time, package time, or runtime:

  • Build-Time Filtering: Pre-filters the corpus into separate static payloads for each tail number. Leanest payload, but requires generating hundreds of builds for a diverse fleet.
  • Package-Time Filtering: Resolves applicability when an offline dataset is deployed to a specific field maintenance tablet.
  • Runtime Filtering: Retains all variations in the client payload so a technician can change equipment options live. Applicability-aware nodes need efficient access to the active configuration without triggering unnecessary work across the document on every change.

The Node-to-Component Mapping

The final step is a dictionary lookup: a node's `type` selects the UI component that renders its data and children. S1000D has dozens of element types across fault isolation, procedural tasks, and maintenance descriptions. A giant switch becomes unreadable quickly; a typed component registry scales better.

type NodeComponent = React.ComponentType<{ node: PublicationNode }>;

const registry: Record<string, NodeComponent> = {
step: ProcedureStep,
warning: WarningBlock,
hotspotFigure: React.lazy(() => import("./HotspotFigure")),
partsListItem: PartsListItem,
// ...one entry per element type in the publication schema
};

function S1000DNode({ node }: { node: PublicationNode }) {
const Component = registry[node.type];
if (!Component) return null; // Unrecognized element: fail in dev, skip in prod
return (
<Component node={node}>
{node.children.map((child, i) => (
<S1000DNode key={child.attrs.id ?? i} node={child} />
))}
</Component>
);
}

The React example shows recursion explicitly; in the abbreviated Angular version, each registered component renders its child nodes through `<s1000d-node>` in its own template.

Each registered component reads applicability context when needed. Heavy components, such as interactive SVG hotspot viewers, can use lazy loading (`React.lazy` or Angular dynamic imports) so they load only when a module uses them.

Where They Actually Differ

The transform loop is the same in both ecosystems. The differences are operational and structural:

  • Change detection: React renders through component trees after state, prop, and context updates; memoization can avoid unnecessary subtree work. Angular can use `OnPush`, Signals, or zoneless change detection for large procedural modules; zone-based checks are a default, not a requirement.
  • Configuration state: Runtime applicability needs global configuration. React uses a root Context provider; Angular uses an injectable service without a template wrapper.
  • Bundle size: Both frameworks support code-splitting and standalone tree-shaking. For offline manuals, the actual bundle difference is usually smaller than expected.
  • Lazy loading: `React.lazy` and Angular dynamic imports both move heavy components—such as 3D viewers or hotspot diagrams—out of the initial payload.

Why the Publication Model Matters for AI

The rise of Large Language Models and maintenance copilots does not make structured publication models obsolete—it makes them indispensable. Feeding raw S1000D XML or flattened PDF content directly to an AI agent can strip away reference and applicability context, increasing the risk of incomplete retrieval, incorrect specifications, and missed safety information.

An autonomous agent or diagnostic assistant benefits from the same structured, graph-resolved data contract that the frontend UI relies on. By resolving references and applicability into a clean canonical model first, the same pipeline can feed both the interactive technician interface and the structured retrieval context for AI assistants.

Engineering leaders should stop debating whether React or Angular will rescue their legacy documentation. Solve reference graph resolution, cycle protection, and runtime applicability in the data model first. Once the canonical model is sound, rendering into a UI component tree—or querying with an AI agent—is straightforward engineering.

Related Reads

Have thoughts on this post?

We welcome discussions and feedback on our architectural observations.

Start a Conversation