diff options
| Commit | 03a882950ead37f74ab4953dbd1199ddbc3d7f6c (patch) | |
|---|---|---|
| Author | 2025-01-01 15:50:49 +0200 | |
| Committer | 2025-01-01 15:50:49 +0200 | |
| Files | 3d47af86ba6694ae60867d308368b932d8b29596 /md-parser.awk | |
| csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.tar csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.tar.gz csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.zip | ||
| Parent | 7f30e62573396b85eb9558faaf943d76ec4750ab (diff) | |
chore: Rename and reorganize files
1 files changed, 491 insertions, 0 deletions
| -rw-r--r-- | md-parser.awk | 491 |
diff --git a/md-parser.awk b/md-parser.awk new file mode 100644 index 0000000..50e7f60 --- /dev/null +++ b/md-parser.awk @@ -0,0 +1,491 @@ +# Almost the entire canonical Markdown specification is supported +# https://daringfireball.net/projects/markdown/syntax +# Notable behavioural exceptions are: +# - Block-level HTML elements needn't be separated with blank lines +# - Lists are identation-aware. List items begin and continue after the same +# identation with which the list beginning (e.g. "* " or "5. ") was written. +# The benefit is that all Markdown is parsed as normal inside an item. +# +# [.] Non-standard features +# [x] Strikethrough ~~...~~ +# [x] Superscript ^...^ +# [x] Subscript ~...~ +# [ ] Footnote [^...] (with user customizations) +# [x] Task list - [ ] (readonly) +# [ ] Definition lists +# [ ] Single line +# [ ] Multi-line +# [x] Fenced code blocks ```lang ``` +# [ ] Tables +# [x] Links +# [x] Link destination (<link> inside anchors) +# [x] Nested links +# +# 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 +} + +BEGIN { + ## This is used to convert email autolinks into a series of hex values + ## Escaped output of: awk 'BEGIN {for (i = 32; i <= 126; ++i){ printf("%c", i) } }' + ascii = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + for (i = 1; i < 97; ++i) { + char_to_dec[char(ascii, i)] = 31 + i + } + + ## We do character escaping by substituting the escape with some + ## "useless" characters, which we're confident won't appear in actual text. + ## These are from decimal value 14 to 28 inclusive. They're blank characters, + ## related to shifting, device control and data transmission. + escapable = "\\`*_{}[]()#+-.!" + for (i = 1; i < 16; ++i) { + escapes[char(escapable, i)] = sprintf("%c", 13 + i) + } +} + +{ + not_paragraph_p1 = not_paragraph + not_paragraph = 0 + empty0 = empty($0) + emptyP1 = empty(P1) + list_started = 0 +} + +# Output is buffered on multiple lines, held in these variables: +# P2 = last non-empty line before the previous line +# P1 = previous line +# $0 = current line +# N1 = next new line (N1 is always set by the user) + +# Inline HTML +/^[ \t]*<\/?[0-9A-Za-z]+/ { + match($0, /^[ \t]*/) + if (last_html || RLENGTH - 2 * list_level < 4) { + last_html = 1 + _next() + } +} +{ + last_html = 0 +} + +# 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 + fenced_class = "" + next + } +} + +# Link definition +!inside_codeblock && /^\[[^\]]+\]:[ \t]+.*/ { + next +} + +# Escapes +!inside_codeblock && /\\[-\\`*_{}\[\]()#+.!]/ { + while (match($0, /\\[-\\`*_{}\[\]()#+.!]/)) { + sub_match(escapes[char($0, RSTART + 1)]) + } +} + +# Horizontal rule +/^(---+|- - -( -)*|\*\*\*+|\* \* \*( \*)*|___+|_ _ _( _)*)$/ { + $0 = "<hr/>" + not_paragraph = 1 +} + +# Blockquotes +blockquote_level > 0 && empty0 { + if (inside_codeblock) { + P1 = P1 ORS "</code></pre>" + inside_codeblock = 0 + } + 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) + + 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 } + +# Lists +function end_list() { + if (list_level == 0) return + PNappnd("</li></" (lists[list_level] == "." ? "o" : "u") "l>") + --list_level +} +function start_list(level, type, shift) { + ## Every two columns we define a new level + level = int((level + 2) / 2) + ## If list definition is the first line inside a code block + if (level - list_level >= 3) return + + ## Make list_level = level - 1 + while (list_level < level - 1) { + lists[++list_level] = "" + } + ## Make list_level = level + while (list_level > level) { + end_list() + } + + if (list_level == level && lists[list_level] != type) { + end_list() + } + if (list_level < level || lists[list_level] != type) { + P1 = P1 ORS "<" (type == "." ? "o" : "u") "l><li>" + lists[level] = type + lists_shift[level] = shift + list_level = level + } + else { + 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) + match($0, /^[^*+-]*[*+-] /) + shift = RLENGTH + sub_match("") + + start_list(level, type, shift) +} +!inside_codeblock && /^[ \t]*[0-9]+\. / { ## Ordered list + match($0, /^[ \t]*/) + level = RLENGTH + type = "." + match($0, /^[^.]+\. /) + shift = RLENGTH + sub_match("") + + 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 + sub("^[ \t]{1," lists_shift[list_level] "}", "") + + while (list_level >= level && emptyP1) { + end_list() + not_paragraph = 0 + } +} + +# Checklists +list_started && /^\[[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 + + match(P1, /```[^ \t]*/) + if (RLENGTH > 3) { + fenced_class = " class=\"language-" tolower(substr(P1, RSTART + 3, RLENGTH - 3)) "\"" + } + + next +} + +# Code blocks +/^( |\t)/ { + ## Autoescape & and < + for (i = 1; match(substr($0, i), /&/); i += RSTART) { + RSTART += i - 1 + sub_match("&") + } + gsub(/</, "\\<") + gsub(/>/, "\\>") + + if (fenced_codeblock) { + $0 = substr($0, 5) + } + if (!inside_codeblock) { + $0 = "<pre><code" fenced_class ">" $0 + inside_codeblock = 1 + } + + _next() +} + +# Automatic links +/<[^ \t<>]+>/ { + ## URL: <uri_scheme:...> + while (match($0, /<[A-Za-z][-A-Za-z0-9+.]*:[^ \t<>]+>/)) { + href = substr($0, RSTART + 1, RLENGTH - 2) + sub_match("<a href=\"" href "\">" href "</a>") + } + + ## Email addresses, with support for quoted local part "..."@... + ## and ip address literal ...@[...] + while (match($0, /<([^ \t<>]+|".+")@([-A-Za-z0-9]+\.[^ \t<>]+|\[[0-9A-Fa-f.:]+\])>/)) { + href = substr($0, RSTART + 1, RLENGTH - 2) + href_masked = "" + ## The original markdown blogpost calls for convertions of all + ## email characters into their hex values. Allegedly, this fools + ## many autoharvesting spam bots. + for (i = length(href); i > 0; --i) { + href_masked = sprintf("&#x%X;", char_to_dec[char(href, i)]) href_masked + } + ## href="mailto:..." + sub_match("<a href=\"mailto:" href_masked "\">" href_masked "</a>") + } +} + +# Autoescape & +/&/ { + for (i = 1; match(substr($0, i), /&/); i += RSTART) { + RSTART += i - 1 + if (substr($0, RSTART) ~ /&[A-Za-z0-9#]+;/) { continue } + sub_match("&") + } +} +# Autoescape < +/ < / { + gsub(/ < /, "\\<") +} + +# Line break +/ $/ { + sub(/[ \t]* $/, "</br>") +} + +# atx-style headers +/^[ \t]*#+/ { + match($0, /#+/) + if (RLENGTH <= 6) { + ## Remove prefix/suffix + gsub(/^[ \t]*#+[ \t]*/, "") + gsub(/[ \t]*#+[ \t]*$/, "") + + $0 = "<h" RLENGTH ">" $0 "</h" RLENGTH ">" + not_paragraph = 1 + } +} + +# setex-style headers +!emptyP1 && /^[ \t]*(=|-)+[ \t]*$/ { + hlevel = 1 + ($0 ~ /-/) + P1 = "<h" hlevel ">" substr(P1, 4) "</h" hlevel ">" + $0 = "" + not_paragraph = 1 +} + +# Emphasis +function surround(brace, left, right, blen) { + if (blen == "") { blen = length(brace) } + if (match($0, brace (brace == "``" ? "." : "[^" brace "]") "+" brace)) { + elem = left substr($0, RSTART + blen, RLENGTH - 2*blen) right + if (brace == "``") { gsub("`", "\\`", elem) } + sub_match(elem) + return 1; + } + return 0 +} +function gsurround(brace, left, right, blen) { + while (surround(brace, left, right, blen)) {} + + if (blen == 1) { + gsub("\\\\", "", brace) + ## Surrounding a single character span with spaces should make it a + ## literal char. This is inelegant but functional. + gsub(" " left " ", " " brace " ") + gsub(" " right " ", " " brace " ") + } +} + +{ + ## Make sure <em> is on the inside for long chains + ## E.g. *****a***** will be the same as ****_a_**** + surround("___" , "<strong><em>", "</em></strong>") + surround("\\*\\*\\*", "<strong><em>", "</em></strong>", 3) + + gsurround("__" , "<strong>", "</strong>") + gsurround("_" , "<em>" , "</em>") + gsurround("\\*\\*", "<strong>", "</strong>", 2) + gsurround("\\*" , "<em>" , "</em>", 1) + gsurround("``" , "<code>" , "</code>") + gsurround("`" , "<code>" , "</code>") + + gsurround("~~" , "<s>" , "</s>") + gsurround("~" , "<sub>" , "</sub>") + gsurround("\\^", "<sup>" , "</sup>", 1) +} + +# Links +/!?\[[^\]]*\] ?[(\[][^)\]]*[\])]/ { + while (match($0, /!?\[[^\]]*\] ?[(\[][^)\]]*[\])]/)) { + link = substr($0, RSTART, RLENGTH) + backup_match() + + ## ](xxxxx) + match(link, /\] ?[(\[][^)\]]*[\])]$/) + has_space = char(link, RSTART + 1) == " " + is_img = char(link, 1) == "!" + + ## ...[xxxxx](...)... + text = substr(link, 2 + is_img, RSTART - 2 - is_img) + ## ...[...](xxxxx)... + href = substr(link, RSTART + has_space + 2, RLENGTH - has_space - 3) + + title = "\"\"" + if (char(link, RSTART + RLENGTH - 1) == "]") { ## is reference link + if (href == "") href = text # [...][] + ## [...]: ... "xxxxx" + title = reference[href, "title"] + ## [...]: xxxxx "..." + href = reference[href, "href"] + } + else if (match(href, /[ \t]"[^"]*"[ \t]*$/)) { ## has title + ## ...[...](... "xxxxx")... + title = substr(href, RSTART + 1, RLENGTH - 1) + ## ...[...](xxxxx "...")... + href = substr(href, 1, RSTART - 1) + } + + if (is_img) elem = "<img src=\"" href "\" alt=\"" text "\" title=" title ">" + else elem = "<a href=\"" href "\" title=" title ">" text "</a>" + + restore_match() + sub_match(elem) + } +} + +# Paragraph +emptyP1 && !empty0 && !not_paragraph { $0 = "<p>" $0 } +!emptyP1 && empty0 && !not_paragraph_p1 { P1 = P1 "</p>" } + +# After all $0 parsing +{ + __buffered_print() +} +function __buffered_print() { + empty0 = empty($0) + emptyP1 = empty(P1) + + if (empty0 && emptyP1) { + ++P1_repeats + } + else if (!empty0 && emptyP1) { + P1 = $0 + } + else { + print P2 + while (P1_repeats > 0) { + print "" + --P1_repeats + } + P2 = P1 + P1 = $0 + if (empty0) { + P1_repeats = 1 + } + } + + if (!empty(N1)) { + if (emptyP1) { + P1 = N1 + } + else { + print P2 + while (P1_repeats > 0) { + print "" + --P1_repeats + } + P2 = P1 + P1 = N1 + N1 = "" + } + } + + ## Fix escaped characters + for (i = 1; i < 16; ++i) { + esc = char(escapable, i) + gsub(escapes[esc], esc, P2) + } +} + +END { + print P2 + while (P1_repeats > 0) { + print "" + --P1_repeats + } + if (!empty(P1)) { print P1 } + print + if (!empty(N1)) { print N1 } +} |
