# 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 } }