Skip to content

Compile hooks

Per-shard transforms run during assembleGuide before sections are stitched. Hooks receive each shard body after heading demotion and preamble stripping. The diagram below shows the steps around the hooks.

Hooks assemble authored GFM. They don’t do variable substitution or template logic. See Preprocessor / templating (out of scope).

assembleGuide (per guide, once for each document it is written to)
│
├─ for each shard (manifest order, then linked shards)
│ ├─ processSection (demote headings, strip about-this-guide)
│ ├─ applyCompileHooks (named hooks from config, in order)
│ ├─ mark the paths codeEvidence rebased
│ ├─ cross-guide pass (automatic when link index present, marks what it writes)
│ ├─ rewriteIntraGuideFileLinks (same-guide links, from the shard directory)
│ └─ rewritePublishRelativeLinks (every guide, relative to its link base, marks what it writes)
│
├─ stitch → stripAnchors (default) → intra-guide .md → unmark
│
└─ markBrokenLinks → realignTables (the last step)

Guide link index: compileGuideResults builds it once from every guide in compileOrder, and the cross-guide pass reads it. Guide link index says which shards have an entry.

Per-assembly hook state: mutable hookState on CompileHookContext (such as inlineInserts counters and first-anchor map) shared across shard invocations within one assembly. Each assembly starts with fresh hookState, and a guide in the monolith has one assembly for each document, as When it runs says. So every hook it runs, a registered custom hook included, sees each of its shards twice, and a hook with side effects beyond its return value repeats them.

The Features Guide’s Compile pipeline specifies the assembly steps that aren’t hooks, and the rules they share with the hooks. Its Path lookup order gives the directories each hook and pass tries when it follows a link to a file.

A custom hook runs only in a process that registers it with registerCompileHook before it calls compileGuideResults or compileGuides. Importing the core registers the built-in hooks and identity, which returns the body unchanged. The Merlon Docs CLI doesn’t load a custom hook, so merlon compile and merlon check compile without one. Register yours in your own build script:

import { registerCompileHook } from '@merlon-docs/merlon-docs-core';
registerCompileHook('myHook', (ctx) => {
return ctx.body.replace(/TODO/g, 'DONE');
});

Then list it in the array form of compile.hooks next to the built-in hooks the guide keeps, such as ["stripAnchors", "codeEvidence", "inlineInserts", "myHook"]. Explicit override says the array replaces the default pipeline, so it has to list them.

Compile skips any name in compile.hooks that has no registered hook, and it doesn’t warn. A misspelled name, such as codeEvidense, compiles as if the list left it out, and no check reports it. Registering a hook under a name already in use replaces that hook, a built-in one included.

A hook gets a CompileHookContext and returns the new body. Assembly always sets hookState, and passes {} as config when the caller doesn’t give one. compileGuideResults sets every other field, and scopeRoot only when the guide sets compile.scopeRoot. A direct assembleGuide call sets outputFile, outputBasename, scopeRoot and linkIndex only when its options give them.

Field Value
body The shard body, after heading demotion, preamble stripping and the hooks listed before this one
guideName The configured name of the guide being compiled
filename The shard’s file name
sourceFile The shard’s absolute path
config The config the caller passed as CompileOptions.config (an MerlonConfigInput), or {}
outputFile Optional. The absolute path of the guide’s link base
outputBasename Optional. The file name of outputFile
scopeRoot Optional. The absolute compile.scopeRoot, unset when the guide doesn’t set it
hookState State that the hook calls of one assembly share, from one shard to the next. Always set, though the type marks it optional
linkIndex Optional. The guide link index

Hook implementations should be pure on ctx.body except when intentionally using shared hookState. Leave unmatched links unchanged.

Built-in hooks run by default, so omit compile.hooks for the common case. Default compile hooks gives the forms compile.hooks takes to turn a hook off or replace the list. Per-hook settings go under guides[].compile.hooksConfig, as inlineInserts config shows, and the config API covers resolveCompileHooks. stripAnchors purpose says what compile.stripAnchors does.

For manifest compile order and compile.sectionsHeading, see Manifest compile order.

  • stripAnchors: per shard. stripAnchors: removes {#id} markers outside code, and every marker on a heading line.
  • codeEvidence: per shard. codeEvidence: repo source links → #L fragments.
  • inlineInserts: per shard. inlineInserts: inline captioned insert libraries.

Assembly also runs the link passes, which aren’t hooks. Its last step, after broken-link marking, is table re-alignment, which prints an aligned table again once its links change width.