From 7f2a2075423fa409e5cbc0903260a899bc47fb9a Mon Sep 17 00:00:00 2001 From: Syndamia Date: Sun, 2 Mar 2025 23:29:25 +0200 Subject: feat(parser)!: Rework --- md-parser.awk | 381 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 247 insertions(+), 134 deletions(-) (limited to 'md-parser.awk') 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("") - 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("") + 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 = "
" - not_paragraph = 1 } # Blockquotes blockquote_level > 0 && empty0 { - if (inside_codeblock) { - P1 = P1 ORS "" - inside_codeblock = 0 - } + STATE_unset(STATE_PARAGRAPH_P1) for (P1 = P1 ORS; blockquote_level > 0; --blockquote_level) { P1 = P1 "" } - 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 "
" } - if (blockquote_level > level) { - if (inside_codeblock) { - P2 = P2 ORS "" - inside_codeblock = 0 - } - P2 = P2 ORS - } for (;blockquote_level > level; --blockquote_level) { P2 = P2 "
" } - 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 = "
" +} # Lists function end_list() { + STATE_unset(STATE_LIST) + if (list_level == 0) return PNappnd("") --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("") P1 = P1 ORS "
  • " } - 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 = "" } # 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 = "
    "
     	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(//, "\\>")
     
    -	if (fenced_codeblock) {
    +	if (STATE_isset(STATE_FENCED_CODEBLOCK)) {
     		$0 = substr($0, 5)
     	}
    -	if (!inside_codeblock) {
    -		$0 = "
    " $0
    -		inside_codeblock = 1
    -	}
    -
    -	_next()
    +}
    +/^(    |\t)/ && /[^ \t]/ { ## New codeblock
    +	STATE_set(STATE_CODEBLOCK)
    +	handle_codeblock()
    +	$0 = "
    " $0
    +	soft_next()
     }
     
     # Definition lists
    -inside_definition_list == 2 && !emptyP1 && ! /^: / {
    -	inside_definition_list = 0
    -	P2 = P2 ""
    -}
    -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 ""
     	}
     
    -	## If nonempty, we're continuing the dd
    -	if (!empty0) {
    -		not_paragraph = 1
    +	if ((!empty0 &&  emptyP1 && /^: /) ||
    +	    ( empty0 && !emptyP1 && !def_started && P1 !~ /^
    /) || + (!empty0 && !emptyP1 && !def_started && !/^: /)) { + STATE_unset(STATE_DEFINITION_LIST) + P2 = P2 "" + + STATE_set(STATE_PARAGRAPH_P1) + STATE_set(STATE_PARAGRAPH) + P1 = "

    " P1 } - else { - PNappnd("

    ") + + if (/^: /) { + def_started = TRUE + } + else if (empty0) { + def_started = FALSE } } /^: / && !emptyP1 { @@ -306,16 +307,18 @@ inside_definition_list == 1 { $0 = substr($0, 3) P1 = "
    " P1 "
    " - if (!inside_definition_list) { + if (STATE_thenset(STATE_DEFINITION_LIST)) { P1 = "
    " P1 } $0 = "
    " $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: @@ -367,7 +370,7 @@ inside_definition_list == 1 { gsub(/[ \t]*#+[ \t]*$/, "") $0 = "" $0 "" - not_paragraph = 1 + STATE_unset(STATE_PARAGRAPH) } } @@ -376,7 +379,7 @@ inside_definition_list == 1 { hlevel = 1 + ($0 ~ /-/) P1 = "" substr(P1, 4) "" $0 = "" - not_paragraph = 1 + STATE_unset(STATE_PARAGRAPH) } # Emphasis @@ -420,11 +423,17 @@ function gsurround(brace, left, right, blen) { gsurround("\\^", "" , "", 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 = "

    " $0 } -!emptyP1 && empty0 && !not_paragraph_p1 { P1 = P1 "

    " } +emptyP1 && !empty0 && STATE_isset(STATE_PARAGRAPH) { $0 = "

    " $0 } +!emptyP1 && empty0 && STATE_isset(STATE_PARAGRAPH_P1) { P1 = P1 "

    " } # After all $0 parsing { @@ -514,7 +523,7 @@ function __buffered_print() { } END { - if (inside_definition_list) { + if (STATE_isset(STATE_DEFINITION_LIST)) { PNappnd("
    ") } @@ -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) +} + -- cgit v1.3.1-23-gd82d