Summary Files Commits Branches @commit @diff @stats
path: root / md-parser.awk
diff options
context:
space:
mode:
Commit03a882950ead37f74ab4953dbd1199ddbc3d7f6c (patch)
Author
 Syndamia <kamen@syndamia.com>
2025-01-01 15:50:49 +0200
Committer
 Syndamia <kamen@syndamia.com>
2025-01-01 15:50:49 +0200
Files3d47af86ba6694ae60867d308368b932d8b29596 /md-parser.awk
csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.tar
csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.tar.gz
csg-03a882950ead37f74ab4953dbd1199ddbc3d7f6c.zip
Parent7f30e62573396b85eb9558faaf943d76ec4750ab (diff)
chore: Rename and reorganize files
1 files changed, 491 insertions, 0 deletions
-rw-r--r--md-parser.awk491
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("&amp;")
+ }
+ gsub(/</, "\\&lt;")
+ gsub(/>/, "\\&gt;")
+
+ 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=\"&#x6D;&#x61;&#x69;&#x6C;&#x74;&#x6F;&#x3A;" 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("&amp;")
+ }
+}
+# Autoescape <
+/ < / {
+ gsub(/ < /, "\\&lt;")
+}
+
+# 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("`", "\\&#96;", 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 }
+}