# Status: # [.] Inline HTML # [x] Autoescape # [x] Block elements # [x] Paragraph # [x] Line break # [x] Headers (atx and setex) # [x] Blockquotes # [x] Lists # [x] Code blocks # [x] Horizontal rules # [x] Span elements # [x] Links # [x] Inline # [x] Reference # [x] Emphasis # [x] Code # [x] Images # [x] Inline # [x] Reference # [x] Other # [x] Autolinks # [x] Escapes # # [ ] Non-standard features # [ ] Strikethrough ~~...~~ # [ ] Superscript ^...^ # [ ] Subscript ~...~ # [ ] Footnote [^...] (with user customizations) # [ ] Task list - [ ] (readonly) # [ ] Definition lists # [x] Single line # [ ] Multi-line # [ ] Fenced code blocks ```lang ``` # [ ] Tables # [ ] Links # [ ] Link destination ( inside anchors) # [ ] Nested links # No character classes, since they aren't supported on every system # # Limitations: # - definition lists always contain one element, i.e. multiple definitions # aren't merged into the same list # - emphasis which isn't surrounded by blanks is still treated as emphasis # e.g. "some_text_here" will become "sometexthere" # - emphasis inside inline code block is still converted to HTML # - backticks inside `` emphasis blocks aren't escaped 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 = previous non-empty 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 } # Link definition !inside_codeblock && /^\[[^\]]+\]:[ \t]+.*/ { next } # Escapes !inside_codeblock && /\\[-\\`*_{}\[\]()#+.!]/ { while (match($0, /\\[-\\`*_{}\[\]()#+.!]/)) { sub_match(escapes[char($0, RSTART + 1)]) } } # Horizontal rule /^(---+|- - -( -)*|\*\*\*+|\* \* \*( \*)*|___+|_ _ _( _)*)$/ { $0 = "
" } if (blockquote_level > level) { 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("" (lists[list_level] == "." ? "o" : "u") "l>") --list_level } function start_list(level, type) { ## 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>
"
inside_codeblock = 1
}
## Autoescape & and <
for (i = 1; match(substr($0, i), /&/); i += RSTART) {
RSTART += i - 1
sub_match("&")
}
gsub(/, "\\<")
_next()
}
inside_codeblock && ! /^[ \t]*$/ {
PNappnd("")
inside_codeblock = 0
emptyP1 = 1
}
# Automatic links
/<[^ \t<>]+>/ {
## URL: " , "")
gsurround("`" , "" , "")
}
# 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 = "" $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 } }