From 03a882950ead37f74ab4953dbd1199ddbc3d7f6c Mon Sep 17 00:00:00 2001 From: Syndamia Date: Wed, 1 Jan 2025 15:50:49 +0200 Subject: chore: Rename and reorganize files --- md-parser.awk | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 md-parser.awk (limited to 'md-parser.awk') 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 ( 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("") + 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 = "
" + not_paragraph = 1 +} + +# Blockquotes +blockquote_level > 0 && empty0 { + if (inside_codeblock) { + P1 = P1 ORS "" + inside_codeblock = 0 + } + 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) + + 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 } + +# Lists +function end_list() { + if (list_level == 0) return + PNappnd("") + --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>
  • " + lists[level] = type + lists_shift[level] = shift + list_level = level + } + else { + 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) + 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 = "" +} + +# 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(//, "\\>") + + if (fenced_codeblock) { + $0 = substr($0, 5) + } + if (!inside_codeblock) { + $0 = "
    " $0
    +		inside_codeblock = 1
    +	}
    +
    +	_next()
    +}
    +
    +# Automatic links
    +/<[^ \t<>]+>/ {
    +	## URL: 
    +	while (match($0, /<[A-Za-z][-A-Za-z0-9+.]*:[^ \t<>]+>/)) {
    +		href = substr($0, RSTART + 1, RLENGTH - 2)
    +		sub_match("" href "")
    +	}
    +
    +	## 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("" href_masked "")
    +	}
    +}
    +
    +# 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]*  $/, "
    ") +} + +# atx-style headers +/^[ \t]*#+/ { + match($0, /#+/) + if (RLENGTH <= 6) { + ## Remove prefix/suffix + gsub(/^[ \t]*#+[ \t]*/, "") + gsub(/[ \t]*#+[ \t]*$/, "") + + $0 = "" $0 "" + not_paragraph = 1 + } +} + +# setex-style headers +!emptyP1 && /^[ \t]*(=|-)+[ \t]*$/ { + hlevel = 1 + ($0 ~ /-/) + P1 = "" substr(P1, 4) "" + $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 is on the inside for long chains + ## E.g. *****a***** will be the same as ****_a_**** + surround("___" , "", "") + surround("\\*\\*\\*", "", "", 3) + + gsurround("__" , "", "") + gsurround("_" , "" , "") + gsurround("\\*\\*", "", "", 2) + gsurround("\\*" , "" , "", 1) + gsurround("``" , "" , "") + gsurround("`" , "" , "") + + gsurround("~~" , "" , "") + gsurround("~" , "" , "") + gsurround("\\^", "" , "", 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 = "\""" + else elem = "" text "" + + restore_match() + sub_match(elem) + } +} + +# Paragraph +emptyP1 && !empty0 && !not_paragraph { $0 = "

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

    " } + +# 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 } +} -- cgit v1.3.1-23-gd82d