ADMONITION_BLOCKQUOTE_RE = /<blockquote>\s*<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*?)<\/blockquote>/m
Captures a blockquote whose first paragraph starts with [!TYPE].
Group 1: type token (uppercased). Group 2: the rest of the blockquote
body, possibly starting with </p> (when the marker was on its own
paragraph) or with the inline body content (when the marker shared a
paragraph with body text via a soft break).
ADMONITION_TYPES = {"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"}
--- GitHub-style Admonitions ---
Recognised types match GitHub's alert syntax.
BARE_MATH_LINE_RE = /\A\x00MATH\d+\x00\z/
A line whose sole content is one math placeholder — the standalone
display-math case, where the emitted
starts at line start and
is a real CommonMark HTML block. Leading blockquote markers are
stripped before the check: > $$x$$ is block position too (the
div becomes an HTML block inside the blockquote).
BLOCKQUOTE_MARKERS_RE = /\A(?:>[ \t]?)+/
CONTAINER_CLOSE_RE = /\A {0,3}:{3,}\z/
CONTAINER_OPEN_RE = /\A {0,3}:{3,}([A-Za-z][\w-]*)[ \t]*(.*)\z/
--- Custom containers (opt-in) ---
:::type Optional Title … ::: blocks (markdown-it/remark
style), emitted with the admonition markup so site CSS is shared:
Title
<blank line — ends the type-6 HTML block, so the body is
parsed as ordinary markdown, fences and task lists included>
…body…
A bare :{3,} run closes the innermost open container, which
gives natural nesting (::::outer / :::inner / ::: /
::::) with a plain counter. Unclosed containers auto-close at
EOF (markdown-it behavior). Fence-aware: ::: lines inside code
fences stay verbatim. The type token is class-safe by
construction; the title is HTML-escaped plain text.
DISPLAY_MATH_RE = InlineMarkdown::DISPLAY_MATH_RE
ENGINE_MARKER_PREFIX = "<!--HWARO-"
Engine-generated marker comments (footnote data blocks, shortcode
placeholders) start with this prefix and must pass through the
transforming passes verbatim: a footnote body containing ~~x~~ or
$x$ lives inside a <!--HWARO-FN:…--> line until postprocess,
and rewriting it there corrupts the data. Author-typed lookalikes
are neutralized to <!-- HWARO- (with a space) by
preprocess_footnotes before this prefix check can match them.
EXISTING_ID_RE = /(?<![\w-])id\s*=\s*"[^"]*"/i
EXISTING_REL_RE = /(?<![\w-])rel\s*=\s*"([^"]*)"/i
EXISTING_TARGET_RE = /(?<![\w-])target\s*=/i
EXTERNAL_ANCHOR_RE = /<a\s((?:[^>"']|"[^"]*"|'[^']*')*)>/i
EXTERNAL_HREF_RE = /(?<![\w-])href\s*=\s*"https?:\/\//i
FOOTNOTE_BLOCK_RE = /\n?<!--HWARO-FOOTNOTES-START-->.*?<!--HWARO-FOOTNOTES-END-->\n?/m
FOOTNOTE_COMMENT_RE = /<!--HWARO-FN:([^:]+):(\d+)(?:\.(\d+))?:(.+?)-->/
Occurrence count rides on the number field as NUM.OCC (e.g. 1.3).
The . separator can't appear in the legacy 3-field NUM: form, so a
3-field comment whose text starts with digits+colon is never misread
as a count.
FOOTNOTE_DEF_RE = /^\[\^([^\]]+)\]:\s*(.+?)$/m
--- Footnotes ---
Pre-processing: extract footnote definitions and replace references with placeholders
FOOTNOTE_REF_RE = /\[\^([^\]]+)\]/
HATTR_MARKER_RE = /<!--HATTR:([0-9a-f]+)-->/
--- Custom Attributes (F9) postprocess ---
Resolves <!--HATTR:HEXPAYLOAD--> markers (left by the preprocess
branches above) into real id/class/other attributes on the
heading tag or <img> tag they trail. Runs BEFORE footnotes (an
attribute block is only ever on a heading/image line, never inside
footnote marker comments) and AFTER heading_ids (so a heading that
got a <!--HID:...--> marker instead — the pure {#id} case — is
already resolved and simply won't match HATTR_MARKER_RE here).
HEADING_ATTR_RE = /^([ ]{0,3})(\#{1,6})[ \t]+(.+?)[ \t]*\{([^{}]+)\}[ \t]*\r?$/
--- Custom Attributes (F9) ---
Generalized {#id .class key=val} attribute blocks — headings and
inline images. See markdown_attributes.cr for the token grammar.
Deliberately broader than HEADING_ID_RE's brace group
([^{}]+ vs \#[A-Za-z][\w:-]*): this is what makes the two
regexes disjoint on ## H {#id} (HEADING_ID_RE wins) while still
catching ## H {#id .class} (falls through to this one, since
HEADING_ID_RE requires the braces to contain ONLY #id).
HEADING_ID_RE = /^([ ]{0,3})(\#{1,6})[ \t]+(.+?)[ \t]*\{\#([A-Za-z][\w:-]*)\}[ \t]*\r?$/
--- Custom Heading IDs ---
## My Heading {#custom-id} → ## My Heading <!--HID:custom-id-->
The marker survives Markd rendering and is converted to an id="..."
attribute in postprocess_heading_ids.
Restricting the id charset to [A-Za-z][\w:-]* keeps it valid as an
HTML id without further escaping.
CommonMark allows up to 3 leading spaces before an ATX heading, which
we capture and preserve so Markd still recognises the line as a heading.
\r? before $: CRLF content otherwise never matches and the id is
silently dropped.
HEADING_TAG_FOR_HID_RE = /<(h[1-6])((?:[^>"']|"[^"]*"|'[^']*')*)>(.*?)<\/\1>/m
Quote-aware attrs (a > inside a quoted value must not end the
tag); the id checks guard with (?<![\w-]) so data-id= never
counts as the element's id.
HID_MARKER_RE = /<!--HID:([A-Za-z][\w:-]*)-->/
HTML_BLOCK_START_RE = /^ {0,3}<\/?(?:address|article|aside|blockquote|caption|center|col|colgroup|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|menu|nav|ol|p|section|summary|table|tbody|td|tfoot|th|thead|tr|ul)(?:[\s>\/]|\r?$)/i
CommonMark "type 6" HTML-block start condition (common block tags,
including the
// markup hwaro itself generates).
A line opening one of these starts a raw-HTML block that runs to
the next blank line — Markd performs NO inline parsing there, so
backslash escapes ship verbatim instead of collapsing.
HTML_CODE_SPAN_RE = /<code(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?>[^<]*<\/code>/
Inline <code>…</code> HTML spans — generated by InlineMarkdown
for table cells / definition bodies (where the original backticks
are already consumed), or author-written raw HTML. Their content
is code: the strikethrough/footnote/math passes must treat it as
opaque, exactly like backtick spans. [^<]* keeps the match to a
flat element (generated spans never contain tags); the attribute
scan is quote-aware so a > inside a quoted value doesn't end
the opening tag early.
IMAGE_ATTR_RE = /(!\[[^\]]*\]\([^)]*\))\{([^{}]+)\}/
{.class key=val} — an attribute block immediately
following an inline image's closing ). Matched inside
transform_outside_code_spans so a literal example in a code span
isn't rewritten.
IMG_HATTR_RE = /(<img\b(?:[^>"']|"[^"]*"|'[^']*')*?)(\s*\/?>)((?:(?!<img\b|<!--HATTR:|<\/(?:p|li|t[dh]|d[dt]|h[1-6])\b).)*?)<!--HATTR:([0-9a-f]+)-->/m
<img ...> opening portion (quote-aware, so a > inside an
attribute value like alt="Home > Docs" isn't mistaken for the
tag end), its closer (> or />, with any whitespace before it),
an optional wrapper the image trails inside (group 3), and the
marker comment this preprocess pass appended. The wrapper is what a
render-image.html hook emits around the <img> (e.g. a
<figure>…</figure>): the marker lands after the whole hook
output, not glued to the <img>, so a naive "marker immediately
follows the tag" match would drop the attributes silently. The
tempered gap stops at the next image or marker, so back-to-back
attributed images each bind their own block, and the no-hook case
(marker glued to the tag) keeps an empty group 3. It also stops at a
closing block tag Markd wraps the image in (</p>, </li>, table
cells, </hN>, …): an image's own marker is always in the same block,
so the gap never legitimately crosses one — this prevents a plain
(marker-less) image from reaching forward and absorbing a later
element's marker (e.g. a heading whose non-conformant hook emitted
non-<hN> markup, leaving its HATTR marker unconsumed by the heading
pass). Hook wrappers (</figure>, </span>, </a>, </picture>, …)
are deliberately NOT excluded, so they still bind normally.
INLINE_MATH_RE = InlineMarkdown::INLINE_MATH_RE
MATH_BODY_MARKD_ACTIVE_RE = /[\\`*_\[\]\-."']/
Markd-active characters inside a math body rendered in normal
inline context (see the branch comments in expand_math). Beyond
emphasis/code/link chars, -, ., and both quotes are included
so markd's opt-in smart punctuation can't rewrite --/.../
quotes inside formulas; when smart is off the backslash escapes
collapse to the same characters, so output is byte-identical.
MATH_PLACEHOLDER_RE = /\x00MATH(\d+)\x00/
SINGLE_LINE_CODE_SPAN_RE = /`[^`\n]+`/
Code-span pattern confined to one line, for stashing inside
multi-line chunks: a stray lone backtick in one paragraph must not
absorb text from another.
STRIKETHROUGH_CODE_RE = /`[^`]+`/
STRIKETHROUGH_RE = InlineMarkdown::INLINE_STRIKETHROUGH_RE
--- Strikethrough (GFM) ---
~~text~~ → <del>text</del>. Markd doesn't ship a GFM strikethrough
parser, so we apply this pre-Markd. The walk is fence-aware so
examples inside fenced code blocks (``` / ~~~) render verbatim,
and inline `code` runs on the same line are skipped via a
placeholder pass so e.g. `~~not strike~~` stays as code.
When math is also enabled, preprocess stashes $…$/$$…$$
spans into opaque placeholders before this pass runs, so $~~x~~$
reaches KaTeX verbatim instead of being rewritten here.
TASK_LIST_ITEM_RE = /<li>(\n?(?:<p>)?)<input type="checkbox"( checked)? disabled>/
--- Task list classes (GFM markup, opt-in) ---
preprocess_task_lists runs before Markd, when the
- doesn't
exist yet — so the GFM classes are added here, on the rendered
HTML. Matches both list shapes markd emits: tight
(
<li><input …) and loose (<li>\n<p><input …). Code blocks
are immune (their <input is entity-escaped).
TASK_LIST_RE = /^(\s*[-*+]\s)\[([ xX])\]/m
--- Task Lists ---
Converts - [ ] and - [x] to checkbox HTML in list items
Instance methods
inline_flags(config : Models::MarkdownConfig) : InlineMarkdown::Flags
Builds the shared InlineMarkdown::Flags for a markdown config —
math plus the F10 opt-in inline markup — so table cells,
definition lists, and footnote bodies all see the same set of
enabled transforms as the main per-line pass above.
Sourcepostprocess(html : String, config : Models::MarkdownConfig) : String
Post-process HTML after Markd rendering
Sourcepostprocess_admonitions(html : String) : String
Post-processing: rewrite GitHub > [!TYPE] blockquotes as admonition divs.
Note: the lazy match against </blockquote> means a nested blockquote
inside the admonition will close the match early. Acceptable for v1 —
GitHub admonitions don't support nested blockquotes either.
Sourcepostprocess_attributes(html : String) : String
Sourcepostprocess_external_links(html : String, config : Models::MarkdownConfig) : String
Sourcepostprocess_heading_ids(html : String) : String
Sourcepostprocess_mermaid(html : String) : String
--- Mermaid ---
Post-processing: convert mermaid code blocks to div elements
Sourcepostprocess_task_list_classes(html : String) : String
Sourcepreprocess(content : String, config : Models::MarkdownConfig) : String
Pre-process markdown content before Markd parsing
Sourcepreprocess_containers(content : String) : String
Sourcepreprocess_definition_lists(content : String, *, math : Bool = false) : String
--- Definition Lists ---
Converts Term\n: Definition syntax to
- HTML.
Fence-aware:
Term / : def lines shown inside a ```/~~~ example
stay verbatim instead of becoming markup inside the code block.
math: true keeps $…$ spans in - /
- bodies untransformed
for the later math pass (see InlineMarkdown.render). Pre-F10
signature — delegates to the
flags overload (existing
callers/specs keep calling this one directly).
Sourcepreprocess_definition_lists(content : String, *, flags : InlineMarkdown::Flags) : String
flags also threads the F10 opt-in inline markup (ins/mark/sub/
sup) into term/definition bodies, alongside the math flag.
Sourcepreprocess_heading_ids(content : String, *, safe : Bool = false) : String
Walk lines and apply the heading-id transform only outside fenced
code blocks, so ## ... {#id} shown inside a ``` example
in the docs renders verbatim.
Under Markd's safe mode, inline HTML comments are replaced with the
placeholder <!-- raw HTML omitted -->, which would both lose the id
and leak that placeholder into the heading text. In that case we
strip the {#id} syntax silently — custom heading IDs are not
supported alongside markdown.safe = true.
Sourcepreprocess_math(content : String) : String
One-shot math transform (stash + immediate expand). preprocess
itself uses the two phases separately so the combined pass runs in
between — see the ordering comment there.
Sourcepreprocess_strikethrough(content : String) : String
Sourcepreprocess_task_lists(content : String) : String
Source