class

Hwaro::Content::Processors::Markdown

Inherits Reference < Object

Markdown processor implementation

Constants

ANCHOR_LINK_REGEX = /<(h[1-6])((?:[^>"'i]++|(?<![\w-])i(?!d="[^"]++")|(?<=[\w-])i|"[^"]*+"|'[^']*+')*+(?<![\w-])id="([^"]+)"#{Utils::TextUtils::HTML_TAG_ATTRS})>(.*?)<\/\1>/m

Regex for matching h1-h6 tags with IDs to insert anchor links. Attribute scans here and below are quote-aware (a > inside a quoted value, e.g. title="a > b", is legal HTML5 and must not end the tag) and the id key uses a (?<![\w-]) guard so data-id= never counts as the element's id. Possessive, so a long unquoted value (an inline data-x=…) does not exhaust the PCRE JIT stack: the prefix stops only at an i that starts a non-empty id="…", the first one the lazy (?:…)*?id="…" scan would have taken.

EMOJI_SKIP_RE = /<(?:(code|pre|script|style|textarea)\b|(span|div)\b[^>]*\bclass="(?:math|mermaid)\b)/

Apply emoji shortcode conversion to HTML text, skipping code, raw-text, math and mermaid elements (see EMOJI_SKIP_RE).

Scans by BYTE offset rather than char index. The tag/fence markers ('<', '>', '<code', '<pre', '', '') and the ':' shortcode delimiters are all ASCII, so byte offsets land exactly on the same boundaries even for UTF-8 text. Char-indexed scanning (html[pos], html.index(_, pos)) is O(n) per access on any string containing a multibyte codepoint, which turned this loop into O(n^2) — a single accented/CJK character on a long page caused a ~1500x slowdown.

HEADING_TAG_REGEX = /<(h([1-6]))(\s#{Utils::TextUtils::HTML_TAG_ATTRS})?>(.*?)<\/h\2>/im

Regex for post_process_html — lightweight replacements for XML.parse_html Matches

…

through
…
, capturing tag name, level digit, attributes, and inner HTML The inner group is .*?, not .+?: an EMPTY heading (## with no text, or one whose only content was a stripped {#id} block) must still match on its own. With .+? the empty <h2></h2> could not match, so the scan slid on and paired that opening tag with the NEXT heading's </h2> — handing the empty heading the following heading's id and leaving the real one with none, which also broke the render-hooks convergence invariant (HookedRenderer#heading gets this right).

ID_ATTR_REGEX = /(?<![\w-])id\s*=\s*["']([^"']+)["']/

Extracts id="value" from an attribute string

IMG_LAZY_REGEX = /<img(?!(?:[^>"'l]++|(?<![\w-])l(?!oading\s*=)|(?<=[\w-])l|"[^"]*+"|'[^']*+')*+(?<![\w-])loading\s*=)((?:[^>"'\s\/]++|\s++(?!\/?>)|\/(?!>)|"[^"]*+"|'[^']*+')*+)\s*\/?>/i

Matches <img ...> tags that do NOT already have a loading= attribute (the lookahead is quote-aware too, so a loading= sitting after a quoted > is still seen, and data-loading= doesn't count). Both scans are possessive, so a long unquoted value (an inline src=data:…) does not exhaust the PCRE JIT stack: the lookahead stops only at an l that starts loading=, and the capture stops at the whitespace / / before the closing > as the lazy (?:…)*?\s*\/?> did.

KNOWN_FRONT_MATTER_KEYS = Set {"title", "description", "image", "draft", "template", "in_sitemap", "toc", "date", "updated", "render", "slug", "path", "aliases", "tags", "transparent", "generate_feeds", "paginate", "pagination_enabled", "sort_by", "reverse", "authors", "in_search_index", "insert_anchor_links", "page_template", "paginate_path", "redirect_to", "weight", "categories", "series", "series_weight", "expires", "paginate_by", "taxonomies", "cascade", "menus", "menu", "translation_key"}

Known front-matter keys (shared between TOML, YAML, and JSON parsers). Using a Set for O(1) lookup instead of Array#includes? O(n).

NON_TAXONOMY_ARRAY_KEYS = Set {"tags", "aliases", "authors"}

Array-typed front matter keys that are NOT taxonomies. These are excluded from automatic taxonomy extraction.

TOML_FRONT_MATTER_REGEX = /\A\+\+\+\s*\n(.*?\n?)^\+\+\+\s*$\n?(.*)\z/m

Regex for TOML front matter

YAML_FRONT_MATTER_REGEX = /\A---\s*\n(.*?\n?)^---\s*$\n?(.*)\z/m

Regex for YAML front matter

YAML_KEY_LINE_RE = /^[\p{L}_][\p{L}\p{N}_.-]*\s*:(\s|$)/

A top-level key: line is what distinguishes broken front matter (worth a hard error) from a document that merely opens with a --- thematic break (whose first block may fail YAML parsing entirely). \p{L} so non-ASCII keys (e.g. Korean 제목:) count as front matter too — otherwise their invalid-YAML blocks silently render as body text instead of erroring.

Instance methods

extra_value(value : TOML::Any | YAML::Any | JSON::Any) : Models::ExtraValue

A front-matter value as page.extra stores it (see extract_extra_value), for callers outside the parser.

Source
parse(raw_content : String, file_path : String = "", declared_keys : Set(String) | Nil = nil)

Returns parsed metadata and content

Source
render(content : String, highlight : Bool = true, safe : Bool = false, lazy_loading : Bool = false, emoji : Bool = false, markdown_config : Models::MarkdownConfig | Nil = nil, hooks : Content::Processors::RenderHooks::HookRenderContext | Nil = nil) : Tuple(String, Array(Models::TocHeader))

Renders Markdown to HTML and generates a Table of Contents Returns {html_content, toc_headers} @param highlight - whether to enable syntax highlighting for code blocks @param safe - if true, raw HTML will not be passed through (replaced by comments) @param lazy_loading - if true, adds loading="lazy" to img tags @param emoji - if true, converts emoji shortcodes to emoji characters @param hooks - render-hook context; nil (the default) renders exactly as before this parameter existed.

Source
render_with_anchors(content : String, highlight : Bool = true, safe : Bool = false, anchor_style : String = "heading", lazy_loading : Bool = false, emoji : Bool = false, markdown_config : Models::MarkdownConfig | Nil = nil, hooks : Content::Processors::RenderHooks::HookRenderContext | Nil = nil) : Tuple(String, Array(Models::TocHeader))

Render with anchor links inserted into headings

Source
typo_suggestion(key : String, candidates : Enumerable(String) = KNOWN_FRONT_MATTER_KEYS) : String | Nil

The closest of candidates to key within Levenshtein distance 2 (but not key itself), or nil. Also the "did you mean" of the [[content.schema]] strict unknown-key error.

Source