Summary Files Commits Branches @commit @diff @stats
path: root / md-parser.awk
diff options
context:
space:
mode:
Commit7f2a2075423fa409e5cbc0903260a899bc47fb9a (patch)
Author
 Syndamia <kamen@syndamia.com>
2025-03-02 23:29:25 +0200
Committer
 Syndamia <kamen@syndamia.com>
2025-03-03 09:47:45 +0200
Files3772a97a2e15ec00f1a1d8ed84b51c823fbcaf1f /md-parser.awk
csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.tar
csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.tar.gz
csg-7f2a2075423fa409e5cbc0903260a899bc47fb9a.zip
Parent43164aa75f8180afdeacf47bc0166ff26195991f (diff)
feat(parser)!: Rework
1 files changed, 247 insertions, 134 deletions
-rw-r--r--md-parser.awk381
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(/</, "\\&lt;")
gsub(/>/, "\\&gt;")
- 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)
+}
+