Hwaro::Services::Importers::HtmlToMarkdown
Lightweight HTML-to-Markdown converter. Handles common HTML elements produced by WordPress and other CMS exports.
Constants
Every spelling of an encoded &: WordPress writes & often.
Cheap short-circuit for the anchor pass: a document with no closing anchor tag has no link to convert.
Innermost-list matcher (see the list pass below). The body is
consumed in CHUNKS — possessive runs of non-< plus single <
guarded against opening a nested list — rather than the previous
per-character (?:(?!<[uo]l[^>]*>).)*?: PCRE2's JIT pushes a
backtrack frame per lazy iteration onto a fixed-size JIT stack,
and per-character iteration crashed the whole conversion with
"JIT stack limit reached" on list bodies only a few thousand
characters long. Chunking makes the frame count scale with the
number of tags instead of characters; the language matched is
identical (only < could ever start a nested-list open, and a
match can only ever end before the < of the closing tag, which
is always a chunk boundary). A {0,n} bound is no alternative —
PCRE2 unrolls bounded GROUP repeats and the pattern blows the
compile-size limit. A tag-spam body can still overflow the JIT
stack, which raises Regex::Error — convert catches that and
falls back to the linear tag strip.
Longest
body the fenced-code pass will match — the
bounded-lazy-body treatment for the one other multi-literal-tail
pass (see MAX_INLINE_BODY_CHARS above), sized up because real code
samples run far longer than inline spans. 65535 is PCRE2's {n,m}
quantifier ceiling. An over-long body keeps its text: the plain
<pre> pass right after has a single-literal tail and no bound.
Longest anchor TEXT the link conversion will match. Bounding the lazy body is what actually makes an unclosed-anchor document linear — see the anchor pass below. Far above any real inline link.
The bound is needed exactly where a pass's tail carries MORE THAN
ONE closing literal (the anchor pass: ["'][^>]*> then </a>;
the pre+code pass: </code> then </pre>). A failed attempt then
rescans between the literals from every opening tag — measured
O(n^2), minutes of CPU inside the 4 MB cap. Passes whose tail is a
SINGLE closing literal ((.*?)</p> and friends) are deliberately
left open: PCRE2 satisfies those in linear time (the required-
literal scan is memoized across bump-along attempts), and bounding
them defeats that optimization — measured ~200x SLOWER on
unclosed-tag spam with a {0,n}? body than with (.*?).
Maximum innermost-list conversion passes (one per nesting level).
Real content nests a handful of levels; a crafted thousand-deep
nest costs a full-document gsub per level, so cap it — deeper
levels fall through to strip_tags, which keeps their text.
Above this size the regex pipeline's lazy quantifiers (e.g. the anchor
body (.*?)</a>) degrade to O(n^2) on adversarial markup (many
unclosed tags). Imported exports are untrusted, so fall back to a
cheap linear tag-strip rather than spend minutes of CPU on a single
crafted item. The threshold is far above any real blog post.