module

Hwaro::Content::Processors::MarkdownAttributes

Constants

CLASS_ATTR_RE = /(?<![\w-])class\s*=\s*"([^"]*)"/i
ID_ATTR_RE = /(?<![\w-])id\s*=\s*"[^"]*"/i

(?<![\w-]) guards keep data-id=/data-class= from counting as the element's own id/class.

ID_PRESENT_RE = /(?<![\w-])id\s*=/i
TOKEN_RE = /\G\s*(?:(\#[\w][\w:-]*)|(\.[A-Za-z_][\w-]*)|([A-Za-z_][\w-]*)=("([^"{}]*)"|[^\s"'`=<>{}]+))/

One token: #id, .class, or key=value/key="quoted value". \G anchors each successive match at the end of the previous one (via the pos argument to match), so a run of intervening whitespace is consumed but any OTHER leftover text — a token that matches none of the three forms — breaks the chain and shows up as residue, which invalidates the whole block.

Capture groups: 1 = #id token (with #), 2 = .class token (with .), 3 = kv key, 4 = kv value (quotes included if quoted), 5 = kv value's inner text when quoted (nil when bare). The #id charset matches HEADING_ID_RE's exactly ([\w][\w:-]*) so {#1-intro} means the same thing whichever of the two mechanisms consumes it. A leading digit is fine for an HTML5 id; .class and key= keep their letter/underscore start, where a leading digit would need CSS escaping / isn't a valid attribute name.

Instance methods

decode(payload : String) : String | Nil

Inverse of encode. nil on any malformed (odd-length / non-hex) payload — never raises.

Source
encode(block_inner : String) : String

Hex-encodes block_inner (the RAW, still-unparsed {...} contents) so it can ride through Markd's render inside an HTML comment without needing its own comment-safe escaping — hex is immune to -->/</& entirely.

Source
merge_attrs(attrs : String, parsed : Parsed) : String

Merges parsed into a heading tag's attribute string ( class="foo", or "" when it had none) or an <img ... tag's opening portion (up to, but NOT including, its />/> closer — the caller re-appends the original closer). id and classes get their own dedicated attribute; every other key=value pair is replaced in place when the tag already carries that attribute, or appended when it doesn't ("source-order replace-or-append"). Every emitted value is HTML-escaped — this is the only place raw {...} payload text reaches tag output, so nothing here may trust it verbatim.

Every sub below passes backreferences: false. The replacement string carries author text, and String#sub(Regex, String) expands \0-\9 (and \k<name>) found INSIDE the replacement against the match. HTML.escape neutralises </>/& but NOT a backslash, so without the flag {alt="use \1 here"} silently loses the \1 and {class="a\1"}/{k="a\0"} splice a capture group — the tag's own existing class, or the whole matched attribute — into the value.

Source
parse(block_inner : String) : Parsed | Nil

Parses the inside of a {...} block (braces already stripped). Returns nil when the block is empty, contains a token matching none of the three forms, or has unconsumed non-whitespace residue between/after valid tokens.

Source

Nested types