diff options
| Commit | 7f2a2075423fa409e5cbc0903260a899bc47fb9a (patch) | |
|---|---|---|
| Author | 2025-03-02 23:29:25 +0200 | |
| Committer | 2025-03-03 09:47:45 +0200 | |
| Files | 3772a97a2e15ec00f1a1d8ed84b51c823fbcaf1f /md-parser.awk | |
| csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.tar csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.tar.gz csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.zip | ||
| Parent | 43164aa75f8180afdeacf47bc0166ff26195991f (diff) | |
feat(parser)!: Rework
1 files changed, 247 insertions, 134 deletions
| -rw-r--r-- | md-parser.awk | 381 |
diff --git a/md-parser.awk b/md-parser.awk index c063b4e..7f3519b 100644 --- a/md-parser.awk +++ b/md-parser.awk @@ -23,40 +23,9 @@ # # No character classes have been used, since they aren't supported on every system -function empty(value) { - return value == "" -} - -function PNappnd(value) { - if (emptyP1) P2 = P2 ORS value - else P1 = P1 ORS value -} - -function _next() { - __buffered_print() - not_paragraph = 1 - next -} - -function sub_match(replacement) { - $0 = substr($0, 1, RSTART - 1) replacement substr($0, RSTART + RLENGTH) -} - -function char(string, ind) { - if (1 > ind || ind > length(string)) { - return ""; - } - return substr(string, ind, 1) -} - -function backup_match() { - backup_rstart = RSTART - backup_rlength = RLENGTH -} -function restore_match() { - RSTART = backup_rstart - RLENGTH = backup_rlength -} +# Naming conventions: +# all caps - constants, do not assign to variable (directly) +# begins with "__" - internal, do not use BEGIN { ## This is used to convert email autolinks into a series of hex values @@ -77,11 +46,11 @@ BEGIN { } { - not_paragraph_p1 = not_paragraph - not_paragraph = 0 + STATE_assign(STATE_PARAGRAPH_P1, STATE_PARAGRAPH) + STATE_set(STATE_PARAGRAPH) + empty0 = empty($0) emptyP1 = empty(P1) - list_started = 0 } # Output is buffered on multiple lines, held in these variables: @@ -90,96 +59,116 @@ BEGIN { # $0 = current line # N1 = next new line (N1 is always set by the user) +#== Block markup ==# + # Inline HTML -/^[ \t]*<\/?[0-9A-Za-z]+/ { - match($0, /^[ \t]*/) - if (last_html || RLENGTH - 2 * list_level < 4) { - last_html = 1 - _next() +/^[ \t]?[ \t]?[ \t]?<\/?[0-9A-Za-z]+/ { + # if it isn't inside a list + if (indentation() - 2 * list_level < 4) { + STATE_set(STATE_HTML) + soft_next() } } -{ - last_html = 0 +STATE_isset(STATE_HTML) { + if (/^[ \t]*$/) { + STATE_unset(STATE_HTML) + } + else { + soft_next() + } } # Fenced code blocks -fenced_codeblock { - $0 = " " $0 -} - -# Code blocks (ending) -inside_codeblock && (/ ```/ || ! /^( |\t|[ \t]*$)/) { - PNappnd("</code></pre>") - inside_codeblock = 0 - emptyP1 = 1 - not_paragraph = 1 - if (fenced_codeblock) { - fenced_codeblock = 0 +STATE_isset(STATE_FENCED_CODEBLOCK) { + if (fenced_class) { + $0 = fenced_class $0 fenced_class = "" - next + soft_next() } + $0 = " " $0 } -# Link definition -!inside_codeblock && /^\[[^\]]+\]:[ \t]+.*/ { - next -} - -# Escapes -!inside_codeblock && /\\[-\\`*_{}\[\]()#+.!^~]/ { - while (match($0, /\\[-\\`*_{}\[\]()#+.!^~]/)) { - sub_match(escapes[char($0, RSTART + 1)]) +# Code blocks (ending condition) +STATE_isset(STATE_CODEBLOCK) { + # If ended codeblock + if ((STATE_isset(STATE_FENCED_CODEBLOCK) && / ```$/) || + ! /^( |\t|[ \t]*$)/ || + (blockquote_level > 0 && empty0)) + { + PNappnd("</code></pre>") + STATE_unset(STATE_CODEBLOCK) + emptyP1 = 1 + if (STATE_thenunset(STATE_FENCED_CODEBLOCK)) { + next + } + } + else if (!STATE_isset(STATE_BLOCKQUOTE)) { + handle_codeblock() + soft_next() } -} - -# Horizontal rule -/^(---+|- - -( -)*|\*\*\*+|\* \* \*( \*)*|___+|_ _ _( _)*)$/ { - $0 = "<hr/>" - not_paragraph = 1 } # Blockquotes blockquote_level > 0 && empty0 { - if (inside_codeblock) { - P1 = P1 ORS "</code></pre>" - inside_codeblock = 0 - } + STATE_unset(STATE_PARAGRAPH_P1) for (P1 = P1 ORS; blockquote_level > 0; --blockquote_level) { P1 = P1 "</blockquote>" } - not_paragraph_p1 = 1 } /^>[ \t]/ { match($0, /^[> \t]*> /) level = substr($0, RSTART, RLENGTH) + $0 = substr($0, RSTART + RLENGTH) gsub(/[ \t]/, "", level) level = length(level) + STATE_set(STATE_BLOCKQUOTE) for (;blockquote_level < level; ++blockquote_level) { P1 = P1 "<blockquote>" } - if (blockquote_level > level) { - if (inside_codeblock) { - P2 = P2 ORS "</code></pre>" - inside_codeblock = 0 - } - P2 = P2 ORS - } for (;blockquote_level > level; --blockquote_level) { P2 = P2 "</blockquote>" } - not_paragraph = 1 } -/^>$/ { $0 = ""; not_paragraph = 1 } +STATE_isset(STATE_BLOCKQUOTE) && $0 == ">" { $0 = "" } + +# Code block (end) +STATE_isset(STATE_CODEBLOCK) { ## when in blockquote + handle_codeblock() + soft_next() +} + +# Link definition +/^\[[^\]]+\]:[ \t]+.*/ { # line starts link definition + next +} + +# Escapes +/\\[-\\`*_{}\[\]()#+.!^~]/ { # has escaped char + while (match($0, /\\[-\\`*_{}\[\]()#+.!^~]/)) { + sub_match(escapes[char($0, RSTART + 1)]) + } +} + +# Horizontal rule +/^(---+|- - -( -)*|\*\*\*+|\* \* \*( \*)*|___+|_ _ _( _)*)$/ { + STATE_unset(STATE_PARAGRAPH) + $0 = "<hr/>" +} # Lists function end_list() { + STATE_unset(STATE_LIST) + if (list_level == 0) return PNappnd("</li></" (lists[list_level] == "." ? "o" : "u") "l>") --list_level } function start_list(level, type, shift) { + STATE_set(STATE_LIST) + brand_new = TRUE + ## Every two columns we define a new level level = int((level + 2) / 2) ## If list definition is the first line inside a code block @@ -207,22 +196,20 @@ function start_list(level, type, shift) { PNappnd("</li>") P1 = P1 ORS "<li>" } - not_paragraph = 1 - list_started = 1 } -!inside_codeblock && /^[ \t]*[*+-] / { ## Unordered list - match($0, /^[ \t]*[*+-] /) - level = RLENGTH - 2 - type = char($0, RSTART + RLENGTH - 2) +/^[ \t]*[*+-] / { ## Unordered list + level = indentation() + match($0, /^[^*+-]*[*+-] /) + type = char($0, RSTART + RLENGTH - 2) shift = RLENGTH sub_match("") start_list(level, type, shift) } -!inside_codeblock && /^[ \t]*[0-9]+\. / { ## Ordered list - match($0, /^[ \t]*/) - level = RLENGTH +/^[ \t]*[0-9]+\. / { ## Ordered list + level = indentation() + type = "." match($0, /^[^.]+\. /) shift = RLENGTH @@ -230,39 +217,45 @@ function start_list(level, type, shift) { start_list(level, type, shift) } -list_level > 0 && !list_started && /[^ \t]/ { ## Non-list line - match($0, /^[ \t]*/) - level = int((RLENGTH + 2) / 2) - not_paragraph = 1 +!brand_new && list_level > 0 && /[^ \t]/ { ## Non-list line + level = int((indentation() + 2) / 2) sub("^[ \t]{1," lists_shift[list_level] "}", "") while (list_level >= level && emptyP1) { end_list() - not_paragraph = 0 } } +list_level > 0 { + STATE_unset(STATE_PARAGRAPH) + brand_new = FALSE +} # Checklists -list_started && /^\[[x ]\][ \t]/ { +STATE_isset(STATE_LIST) && /^\[[x ]\][ \t]/ { checked = (char($0, 2) == "x") ? " checked=\"checked\"" : ""; $0 = "<input type=\"checkbox\" disabled=\"disabled\"" checked \ "><label>" substr($0, 5) "</label>" } # Fenced code blocks -!inside_codeblock && /^[ \t]*```/ { - fenced_codeblock = 1 +/^[ \t]*```/ { + STATE_set(STATE_FENCED_CODEBLOCK) + STATE_set(STATE_CODEBLOCK) match(P1, /```[^ \t]*/) if (RLENGTH > 3) { fenced_class = " class=\"language-" tolower(substr(P1, RSTART + 3, RLENGTH - 3)) "\"" } + else { + fenced_class = "" + } + fenced_class = "<pre><code" fenced_class ">" next } # Code blocks -/^( |\t)/ { +function handle_codeblock() { ## Autoescape & and < for (i = 1; match(substr($0, i), /&/); i += RSTART) { RSTART += i - 1 @@ -271,34 +264,42 @@ list_started && /^\[[x ]\][ \t]/ { gsub(/</, "\\<") gsub(/>/, "\\>") - if (fenced_codeblock) { + if (STATE_isset(STATE_FENCED_CODEBLOCK)) { $0 = substr($0, 5) } - if (!inside_codeblock) { - $0 = "<pre><code" fenced_class ">" $0 - inside_codeblock = 1 - } - - _next() +} +/^( |\t)/ && /[^ \t]/ { ## New codeblock + STATE_set(STATE_CODEBLOCK) + handle_codeblock() + $0 = "<pre><code>" $0 + soft_next() } # Definition lists -inside_definition_list == 2 && !emptyP1 && ! /^: / { - inside_definition_list = 0 - P2 = P2 "</dl>" -} -inside_definition_list == 1 { - ## If empty, get ready to close dl - if ($0 !~ /[^ \t]/) { - ++inside_definition_list +STATE_isset(STATE_DEFINITION_LIST) { + if (!empty0) { + STATE_unset(STATE_PARAGRAPH) + } + else if (def_started) { + P1 = P1 "</dd>" } - ## If nonempty, we're continuing the dd - if (!empty0) { - not_paragraph = 1 + if ((!empty0 && emptyP1 && /^: /) || + ( empty0 && !emptyP1 && !def_started && P1 !~ /^<dd>/) || + (!empty0 && !emptyP1 && !def_started && !/^: /)) { + STATE_unset(STATE_DEFINITION_LIST) + P2 = P2 "</dl>" + + STATE_set(STATE_PARAGRAPH_P1) + STATE_set(STATE_PARAGRAPH) + P1 = "<p>" P1 } - else { - PNappnd("</dd>") + + if (/^: /) { + def_started = TRUE + } + else if (empty0) { + def_started = FALSE } } /^: / && !emptyP1 { @@ -306,16 +307,18 @@ inside_definition_list == 1 { $0 = substr($0, 3) P1 = "<dt>" P1 "</dt>" - if (!inside_definition_list) { + if (STATE_thenset(STATE_DEFINITION_LIST)) { P1 = "<dl>" P1 } $0 = "<dd>" $0 + def_started = TRUE - inside_definition_list = 1 - not_paragraph = 1 - not_paragraph_p1 = 1 + STATE_unset(STATE_PARAGRAPH) + STATE_unset(STATE_PARAGRAPH_P1) } +#== Inline markup ==# + # Automatic links /<[^ \t<>]+>/ { ## URL: <uri_scheme:...> @@ -367,7 +370,7 @@ inside_definition_list == 1 { gsub(/[ \t]*#+[ \t]*$/, "") $0 = "<h" RLENGTH ">" $0 "</h" RLENGTH ">" - not_paragraph = 1 + STATE_unset(STATE_PARAGRAPH) } } @@ -376,7 +379,7 @@ inside_definition_list == 1 { hlevel = 1 + ($0 ~ /-/) P1 = "<h" hlevel ">" substr(P1, 4) "</h" hlevel ">" $0 = "" - not_paragraph = 1 + STATE_unset(STATE_PARAGRAPH) } # Emphasis @@ -420,11 +423,17 @@ function gsurround(brace, left, right, blen) { gsurround("\\^", "<sup>" , "</sup>", 1) } +# Footnote references +/\[\^[^\]]+\]/ { + while (match($0, /\[\^[\]]+\]/)) { + } +} + # Links /!?\[[^\]]*\] ?[(\[][^)\]]*[\])]/ { while (match($0, /!?\[[^\]]*\] ?[(\[][^)\]]*[\])]/)) { link = substr($0, RSTART, RLENGTH) - backup_match() + save_match() ## ](xxxxx) match(link, /\] ?[(\[][^)\]]*[\])]$/) @@ -460,8 +469,8 @@ function gsurround(brace, left, right, blen) { } # Paragraph -emptyP1 && !empty0 && !not_paragraph { $0 = "<p>" $0 } -!emptyP1 && empty0 && !not_paragraph_p1 { P1 = P1 "</p>" } +emptyP1 && !empty0 && STATE_isset(STATE_PARAGRAPH) { $0 = "<p>" $0 } +!emptyP1 && empty0 && STATE_isset(STATE_PARAGRAPH_P1) { P1 = P1 "</p>" } # After all $0 parsing { @@ -514,7 +523,7 @@ function __buffered_print() { } END { - if (inside_definition_list) { + if (STATE_isset(STATE_DEFINITION_LIST)) { PNappnd("</dl>") } @@ -527,3 +536,107 @@ END { print if (!empty(N1)) { print N1 } } + +#== Boolean "constants" ==# +BEGIN { + FALSE = 0 + TRUE = 1 +} + +#== States system ==# +# To convey the current state of parsing, we're using "states" +# These are nothing more than variables and an array, storing "booleans" +# The main benefit is semantics and syntax usage +# (especially since we're using variables, editors will offer autocompletion) + +BEGIN { + # State definitions + STATE_HTML = 0 + STATE_FENCED_CODEBLOCK = 1 + STATE_CODEBLOCK = 2 + STATE_BLOCKQUOTE = 3 + STATE_PARAGRAPH = 4 + STATE_PARAGRAPH_P1 = 5 + STATE_LIST = 6 + STATE_DEFINITION_LIST = 7 +} +function STATE_set(name) { + STATE_unset(STATE_PARAGRAPH) + __state[name] = TRUE +} +function STATE_assign(name, other) { + __state[name] = __state[other] +} +function STATE_unset(name) { + __state[name] = FALSE +} +function STATE_isset(name) { + return __state[name] +} +function STATE_thenset(name) { + if (!__state[name]) { + STATE_set(name) + return TRUE + } + return FALSE +} +function STATE_thenunset(name) { + if (__state[name]) { + STATE_unset(STATE_PARAGRAPH) + __state[name] = FALSE + return TRUE + } + return FALSE +} + +#== Line information ==# + +function indentation( len) { + save_match() + match($0, /^[ \t]*/) + len = RLENGTH + restore_match() + return len +} + +#== Match handling ==# + +function save_match() { + backup_rstart = RSTART + backup_rlength = RLENGTH +} +function restore_match() { + RSTART = backup_rstart + RLENGTH = backup_rlength +} + + +#== Other ==# + +function empty(value) { + return value == "" +} + +function PNappnd(value) { + if (emptyP1) P2 = P2 ORS value + else P1 = P1 ORS value +} + +function soft_next() { + __buffered_print() + + STATE_unset(STATE_PARAGRAPH) + next +} + +function sub_match(replacement) { + $0 = substr($0, 1, RSTART - 1) replacement substr($0, RSTART + RLENGTH) +} + +function char(string, ind) { + if (1 > ind || ind > length(string)) { + return ""; + } + return substr(string, ind, 1) +} + |
