Commit 5479d3a h
Changes: 9 files +26 -1034
Changes (Side-by-Side)
.gitmodules +3 -0
| |||
| 1 | [submodule "share/jenny/lib/markdown.awk"] |
||
| 2 | path = share/jenny/lib/markdown.awk |
||
| 3 | url = git://grit.deadkey.org/mailmanpat/markdown.awk.git |
||
bin/jenny +1 -1
if source_blogrc; then | |||
| 142 | [ -z "${POSTS_PER_PAGE:-}" ] && POSTS_PER_PAGE=10 |
142 | [ -z "${POSTS_PER_PAGE:-}" ] && POSTS_PER_PAGE=10 |
| 143 | [ -z "${DIST:-}" ] && DIST=$PWD/.dist |
143 | [ -z "${DIST:-}" ] && DIST=$PWD/.dist |
| 144 | [ -z "${LAYOUT_DIR:-}" ] && LAYOUT_DIR=$SHARE_DIR/layout |
144 | [ -z "${LAYOUT_DIR:-}" ] && LAYOUT_DIR=$SHARE_DIR/layout |
| 145 | [ -z "${MARKDOWN_COMMAND:-}" ] && MARKDOWN_COMMAND="\$AWK -f \$SHARE_DIR/lib/md2html.awk" |
||
| 145 | [ -z "${MARKDOWN_COMMAND:-}" ] && MARKDOWN_COMMAND="\$AWK -f \$SHARE_DIR/lib/markdown.awk/markdown.awk" |
||
| 146 | [ -z "${FEED_MAX_ITEMS:-}" ] && FEED_MAX_ITEMS=20 |
146 | [ -z "${FEED_MAX_ITEMS:-}" ] && FEED_MAX_ITEMS=20 |
| 147 | [ -z "${URL_STYLE:-}" ] && URL_STYLE=html |
147 | [ -z "${URL_STYLE:-}" ] && URL_STYLE=html |
| 148 | |
148 | |
share/jenny/layout/page.sh +1 -1
cat << _EOF_ | |||
| 978 | .contents h1:not(:is(h1, h2) ~ *)::before { content: none; } |
978 | .contents h1:not(:is(h1, h2) ~ *)::before { content: none; } |
| 979 | ol, ul { padding-left: 1em; } |
979 | ol, ul { padding-left: 1em; } |
| 980 | /* Notes and references are two different lists, so each is labelled. |
980 | /* Notes and references are two different lists, so each is labelled. |
| 981 | The wording lives here rather than in the parser: md2html.awk marks |
||
| 981 | The wording lives here rather than in the parser: markdown.awk marks |
||
| 982 | them with classes and leaves naming to the theme, which keeps |
982 | them with classes and leaves naming to the theme, which keeps |
| 983 | English out of the parser and lets a theme relabel or hide either |
983 | English out of the parser and lets a theme relabel or hide either |
| 984 | by overriding one rule. Styled to match an h5. */ |
984 | by overriding one rule. Styled to match an h5. */ |
share/jenny/layout/post.sh +1 -1
cat << _EOF_ | |||
| 799 | .contents h1:not(:is(h1, h2) ~ *)::before { content: none; } |
799 | .contents h1:not(:is(h1, h2) ~ *)::before { content: none; } |
| 800 | ol, ul { padding-left: 1em; } |
800 | ol, ul { padding-left: 1em; } |
| 801 | /* Notes and references are two different lists, so each is labelled. |
801 | /* Notes and references are two different lists, so each is labelled. |
| 802 | The wording lives here rather than in the parser: md2html.awk marks |
||
| 802 | The wording lives here rather than in the parser: markdown.awk marks |
||
| 803 | them with classes and leaves naming to the theme, which keeps |
803 | them with classes and leaves naming to the theme, which keeps |
| 804 | English out of the parser and lets a theme relabel or hide either |
804 | English out of the parser and lets a theme relabel or hide either |
| 805 | by overriding one rule. Styled to match an h5. */ |
805 | by overriding one rule. Styled to match an h5. */ |
share/jenny/lib/helpers.sh +11 -2
function build_signature() { | |||
| 211 | # surely as a template does: adding ids to headings changed the output of |
211 | # surely as a template does: adding ids to headings changed the output of |
| 212 | # every page, and without this the lock skipped them all -- the build said |
212 | # every page, and without this the lock skipped them all -- the build said |
| 213 | # nothing had changed while the change was in the markdown parser. |
213 | # nothing had changed while the change was in the markdown parser. |
| 214 | find "${JENNY_LIB_DIR:-}" -maxdepth 1 -type f \( -name '*.sh' -o -name '*.awk' \) \ |
||
| 214 | # |
||
| 215 | # No -maxdepth: the parser is a submodule now, one directory down, and a |
||
| 216 | # depth-limited scan stopped seeing it. Verified before the fix -- editing |
||
| 217 | # the parser left the signature identical, which is the same silent |
||
| 218 | # skip-everything this line was added to prevent, returned by a different |
||
| 219 | # route. The submodule's own tests are excluded: they change without |
||
| 220 | # changing a single rendered byte, and hashing them would rebuild the |
||
| 221 | # whole site for a test edit. |
||
| 222 | find "${JENNY_LIB_DIR:-}" -type f \( -name '*.sh' -o -name '*.awk' \) \ |
||
| 223 | -not -path '*/test/*' \ |
||
| 215 | -print0 2>/dev/null \ |
224 | -print0 2>/dev/null \ |
| 216 | | LC_ALL=C sort -z \ |
225 | | LC_ALL=C sort -z \ |
| 217 | | xargs -0 cat 2>/dev/null |
226 | | xargs -0 cat 2>/dev/null |
function read_nav() { | |||
| 848 | # |
857 | # |
| 849 | # The parser owns what counts as metadata and wraps each pair in the rendered |
858 | # The parser owns what counts as metadata and wraps each pair in the rendered |
| 850 | # output. jenny reads the same block from the source to hydrate variables the |
859 | # output. jenny reads the same block from the source to hydrate variables the |
| 851 | # layout can use, so there is one definition of the syntax, in md2html.awk, |
||
| 860 | # layout can use, so there is one definition of the syntax, in markdown.awk, |
||
| 852 | # and this stays a straight read of the same shape. |
861 | # and this stays a straight read of the same shape. |
| 853 | |
862 | |
| 854 | # get_metadata <file> |
863 | # get_metadata <file> |
share/jenny/lib/markdown.awk +1 -0
| |||
| 1 | Subproject commit 8cd72df31b3ae05a98735b419311da6274329401 |
||
share/jenny/lib/md2html.awk +0 -1027
| |||
| 1 | #!/bin/awk -f |
||
| 2 | # |
||
| 3 | # by: Jesus Galan (yiyus) 2009 |
||
| 4 | # |
||
| 5 | # Usage: md2html.awk file.md > file.html |
||
| 6 | # See: http://4l77.com/src/md2html.awk |
||
| 7 | |
||
| 8 | # Attribute values need quotes escaped as well as the usual entities. |
||
| 9 | function escattr(t) { |
||
| 10 | t = eschtml(t); |
||
| 11 | gsub(">", "\\>", t); |
||
| 12 | gsub("\"", "\\"", t); |
||
| 13 | return t; |
||
| 14 | } |
||
| 15 | |
||
| 16 | # Fenced content is fully literal, so > is escaped too. eschtml is shared with |
||
| 17 | # inline parsing, where a bare > is legitimate text, so it is left alone. |
||
| 18 | function escfence(t) { |
||
| 19 | t = eschtml(t); |
||
| 20 | gsub(">", "\\>", t); |
||
| 21 | return t; |
||
| 22 | } |
||
| 23 | |
||
| 24 | function eschtml(t) { |
||
| 25 | gsub("&", "\\&", t); |
||
| 26 | gsub("<", "\\<", t); |
||
| 27 | return t; |
||
| 28 | } |
||
| 29 | |
||
| 30 | function oprint(t){ |
||
| 31 | if(nr == 0) |
||
| 32 | print t; |
||
| 33 | else |
||
| 34 | otext = otext "\n" t; |
||
| 35 | } |
||
| 36 | |
||
| 37 | function subref(id){ |
||
| 38 | for(; nr > 0 && sub("<<" id, ref[id], otext); nr--); |
||
| 39 | if(nr == 0 && otext) { |
||
| 40 | print otext; |
||
| 41 | otext = ""; |
||
| 42 | } |
||
| 43 | } |
||
| 44 | |
||
| 45 | # Escapes regex metacharacters so a literal string can be interpolated into |
||
| 46 | # a dynamic regex. Without this "^" "*" reaches the regex engine, which mawk |
||
| 47 | # rejects outright and gawk quietly mismatches. |
||
| 48 | function re_quote(s, out, i, c) { |
||
| 49 | out = ""; |
||
| 50 | for(i = 1; i <= length(s); i++) { |
||
| 51 | c = substr(s, i, 1); |
||
| 52 | if(index("\\^$.[]|()*+?{}/", c) > 0) |
||
| 53 | out = out "\\" c; |
||
| 54 | else |
||
| 55 | out = out c; |
||
| 56 | } |
||
| 57 | return out; |
||
| 58 | } |
||
| 59 | |
||
| 60 | function nextil(t) { |
||
| 61 | if(!match(t, /[`<&\[*_~\\-]|(!\[)|(\[\^)|(\[#)/)) |
||
| 62 | return t; |
||
| 63 | t1 = substr(t, 1, RSTART - 1); |
||
| 64 | tag = substr(t, RSTART, RLENGTH); |
||
| 65 | t2 = substr(t, RSTART + RLENGTH); |
||
| 66 | if(ilcode && tag != "`") |
||
| 67 | return eschtml(t1 tag) nextil(t2); |
||
| 68 | # Backslash escaping |
||
| 69 | if(tag == "\\"){ |
||
| 70 | if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){ |
||
| 71 | tag = substr(t2, 1, 1); |
||
| 72 | t2 = substr(t2, 2); |
||
| 73 | } |
||
| 74 | return t1 tag nextil(t2); |
||
| 75 | } |
||
| 76 | # Dashes |
||
| 77 | if(tag == "-"){ |
||
| 78 | if(sub(/^-/, "", t2)) |
||
| 79 | tag = "—"; |
||
| 80 | return t1 tag nextil(t2); |
||
| 81 | } |
||
| 82 | # Inline Code |
||
| 83 | if(tag == "`"){ |
||
| 84 | if(sub(/^`/, "", t2)){ |
||
| 85 | if(!match(t2, /``/)) |
||
| 86 | return t1 "”" nextil(t2); |
||
| 87 | ilcode2 = !ilcode2; |
||
| 88 | } |
||
| 89 | else if(ilcode2) |
||
| 90 | return t1 tag nextil(t2); |
||
| 91 | tag = "<code>"; |
||
| 92 | if(ilcode){ |
||
| 93 | t1 = eschtml(t1); |
||
| 94 | tag = "</code>"; |
||
| 95 | } |
||
| 96 | ilcode = !ilcode; |
||
| 97 | return t1 tag nextil(t2); |
||
| 98 | } |
||
| 99 | if(tag == "<"){ |
||
| 100 | # Autolinks |
||
| 101 | if(match(t2, /^[^ ]+[\.@][^ ]+>/)){ |
||
| 102 | url = eschtml(substr(t2, 1, RLENGTH - 1)); |
||
| 103 | t2 = substr(t2, RLENGTH + 1); |
||
| 104 | linktext = url; |
||
| 105 | if(match(url, /@/) && !match(url, /^mailto:/)) |
||
| 106 | url = "mailto:" url; |
||
| 107 | return t1 "<a href=\"" url "\">" linktext "</a>" nextil(t2); |
||
| 108 | } |
||
| 109 | # Html tags |
||
| 110 | if(match(t2, /^[A-Za-z\/!][^>]*>/)){ |
||
| 111 | tag = tag substr(t2, RSTART, RLENGTH); |
||
| 112 | t2 = substr(t2, RLENGTH + 1); |
||
| 113 | return t1 tag nextil(t2); |
||
| 114 | } |
||
| 115 | return t1 "<" nextil(t2); |
||
| 116 | } |
||
| 117 | # Html special entities |
||
| 118 | if(tag == "&"){ |
||
| 119 | if(match(t2, /^#?[A-Za-z0-9]+;/)){ |
||
| 120 | tag = tag substr(t2, RSTART, RLENGTH); |
||
| 121 | t2 = substr(t2, RLENGTH + 1); |
||
| 122 | return t1 tag nextil(t2); |
||
| 123 | } |
||
| 124 | return t1 "&" nextil(t2); |
||
| 125 | } |
||
| 126 | # Images |
||
| 127 | if(tag == "!["){ |
||
| 128 | if(!match(t2, /(\[.*\])|(\(.*\))/)) |
||
| 129 | return t1 tag nextil(t2); |
||
| 130 | match(t2, /^[^\]]*/); |
||
| 131 | alt = substr(t2, 1, RLENGTH); |
||
| 132 | t2 = substr(t2, RLENGTH + 2); |
||
| 133 | if(match(t2, /^\(/)){ |
||
| 134 | # Inline |
||
| 135 | sub(/^\(/, "", t2); |
||
| 136 | match(t2, /^[^\)]+/); |
||
| 137 | url = eschtml(substr(t2, 1, RLENGTH)); |
||
| 138 | t2 = substr(t2, RLENGTH + 2); |
||
| 139 | title = ""; |
||
| 140 | if(match(url, /[ ]+".*"[ ]*$/)) { |
||
| 141 | title = substr(url, RSTART, RLENGTH); |
||
| 142 | url = substr(url, 1, RSTART - 1); |
||
| 143 | match(title, /".*"/); |
||
| 144 | title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; |
||
| 145 | } |
||
| 146 | if(match(url, /^<.*>$/)) |
||
| 147 | url = substr(url, 2, RLENGTH - 2); |
||
| 148 | return t1 "<img src=\"" url "\" alt=\"" alt "\"" title " />" nextil(t2); |
||
| 149 | } |
||
| 150 | else{ |
||
| 151 | # Referenced |
||
| 152 | sub(/^ ?\[/, "", t2); |
||
| 153 | id = alt; |
||
| 154 | if(match(t2, /^[^\]]+/)) |
||
| 155 | id = substr(t2, 1, RLENGTH); |
||
| 156 | t2 = substr(t2, RLENGTH + 2); |
||
| 157 | if(ref[id]) |
||
| 158 | r = ref[id]; |
||
| 159 | else{ |
||
| 160 | r = "<<" id; |
||
| 161 | nr++; |
||
| 162 | } |
||
| 163 | return t1 "<img src=\"" r "\" alt=\"" alt "\" />" nextil(t2); |
||
| 164 | } |
||
| 165 | } |
||
| 166 | # Citations |
||
| 167 | # |
||
| 168 | # [#id] or [locator][#id]. Numbered by first use; a repeat reuses the |
||
| 169 | # number, matching multimarkdown. |
||
| 170 | if(tag == "[#"){ |
||
| 171 | match(t2, /^[^\]]*/); |
||
| 172 | cid = substr(t2, 1, RLENGTH); |
||
| 173 | t2 = substr(t2, RLENGTH + 2); |
||
| 174 | if(!(cid in cite_num)){ |
||
| 175 | cite_num[cid] = ++cite_n; |
||
| 176 | cite_order[cite_n] = cid; |
||
| 177 | } |
||
| 178 | cnum = cite_num[cid]; |
||
| 179 | # A locator set by the link branch for the [locator][#id] form. |
||
| 180 | cloc = cite_locator; |
||
| 181 | cite_locator = ""; |
||
| 182 | clabel = (cloc != "") ? "(" cloc ", " cnum ")" : "(" cnum ")"; |
||
| 183 | return t1 "<a href=\"#cn:" cnum "\" id=\"cnref:" cnum "\" data-citation=\"" cid \ |
||
| 184 | "\" title=\"see citation\" class=\"citation\">" clabel "</a>" nextil(t2); |
||
| 185 | } |
||
| 186 | |
||
| 187 | # Footnotes |
||
| 188 | if(tag == "[^"){ |
||
| 189 | match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); |
||
| 190 | linktext = substr(t2, 1, RLENGTH); |
||
| 191 | t2 = substr(t2, RLENGTH + 2); |
||
| 192 | return t1 "<sup class=\"fnref\" data-fnref=\"" linktext "\"><a href=\"#fn-" linktext "\" id=\"fnref-" linktext "\">" linktext "</a></sup>" nextil(t2); |
||
| 193 | } |
||
| 194 | # Links |
||
| 195 | if(tag == "["){ |
||
| 196 | # A shortcut reference is a bare [text] with no following [] or (). |
||
| 197 | # It is only a link if a definition matching the text exists, so the |
||
| 198 | # early return here must not fire for one -- it is decided below. |
||
| 199 | if(!match(t2, /(\[.*\])|(\(.*\))/)){ |
||
| 200 | if(!match(t2, /^[^\]]*\]/)) |
||
| 201 | return t1 tag nextil(t2); |
||
| 202 | shortcut_candidate = 1; |
||
| 203 | } |
||
| 204 | else |
||
| 205 | shortcut_candidate = 0; |
||
| 206 | match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); |
||
| 207 | linktext = substr(t2, 1, RLENGTH); |
||
| 208 | t2 = substr(t2, RLENGTH + 2); |
||
| 209 | if(match(t2, /^\(/)){ |
||
| 210 | # Inline |
||
| 211 | match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/); |
||
| 212 | url = substr(t2, 2, RLENGTH - 1); |
||
| 213 | pt2 = substr(t2, RLENGTH + 2); |
||
| 214 | title = ""; |
||
| 215 | if(match(url, /[ ]+".*"[ ]*$/)) { |
||
| 216 | title = substr(url, RSTART, RLENGTH); |
||
| 217 | url = substr(url, 1, RSTART - 1); |
||
| 218 | match(title, /".*"/); |
||
| 219 | title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; |
||
| 220 | } |
||
| 221 | if(match(url, /^<.*>$/)) |
||
| 222 | url = substr(url, 2, RLENGTH - 2); |
||
| 223 | url = eschtml(url); |
||
| 224 | return t1 "<a href=\"" url "\"" title ">" nextil(linktext) "</a>" nextil(pt2); |
||
| 225 | } |
||
| 226 | else{ |
||
| 227 | # Referenced: [text][id], [text][] or the shortcut [text]. |
||
| 228 | id = linktext; |
||
| 229 | shortcut = 1; |
||
| 230 | if(match(t2, /^ ?\[#/)){ |
||
| 231 | # [locator][#id] is a citation, not a reference link. The |
||
| 232 | # locator is handed to the citation branch directly; nextil |
||
| 233 | # has already split t1 away, so it cannot be recovered there. |
||
| 234 | sub(/^ ?\[/, "", t2); |
||
| 235 | cite_locator = linktext; |
||
| 236 | return t1 nextil("[" t2); |
||
| 237 | } |
||
| 238 | if(match(t2, /^ ?\[/)){ |
||
| 239 | shortcut = 0; |
||
| 240 | sub(/^ ?\[/, "", t2); |
||
| 241 | if(match(t2, /^[^\]]*/)){ |
||
| 242 | # An empty [] means "reuse the link text as the label". |
||
| 243 | if(RLENGTH > 0) |
||
| 244 | id = substr(t2, 1, RLENGTH); |
||
| 245 | t2 = substr(t2, RLENGTH + 2); |
||
| 246 | } |
||
| 247 | } |
||
| 248 | id = tolower(id); |
||
| 249 | if(ref[id]) |
||
| 250 | r = ref[id]; |
||
| 251 | else{ |
||
| 252 | # The definition may appear later in the document, so the |
||
| 253 | # link is emitted with a placeholder that subref() fills in |
||
| 254 | # when the definition is read. A shortcut [text] that is |
||
| 255 | # still unresolved at EOF is restored to literal text by |
||
| 256 | # the END rule. |
||
| 257 | r = "<<" id; |
||
| 258 | nr++; |
||
| 259 | if(shortcut) |
||
| 260 | shortcut_ref[id] = linktext; |
||
| 261 | } |
||
| 262 | pt2 = t2; |
||
| 263 | return t1 "<a href=\"" r "\">" nextil(linktext) "</a>" nextil(pt2); |
||
| 264 | } |
||
| 265 | } |
||
| 266 | # Strikethrough |
||
| 267 | # |
||
| 268 | # Only the doubled form, which is what GFM defines and what authors |
||
| 269 | # write. A single ~ stays literal so that "~/path" and "approx ~5" are |
||
| 270 | # not silently turned into markup. An unpaired "~~" is left alone rather |
||
| 271 | # than opening a <del> that would run to the end of the document. |
||
| 272 | if(tag == "~"){ |
||
| 273 | if(sub(/^~/, "", t2)){ |
||
| 274 | if(match(t2, /~~/)){ |
||
| 275 | strike = substr(t2, 1, RSTART - 1); |
||
| 276 | # t1, tag and t2 are globals shared with every nextil frame, |
||
| 277 | # so parsing the struck text clobbers the tail still waiting |
||
| 278 | # here -- it came back with whatever the inner call left |
||
| 279 | # behind, dropping the rest of the line. The link handler |
||
| 280 | # above keeps its own copy for the same reason. |
||
| 281 | strike_t1 = t1; |
||
| 282 | strike_rest = substr(t2, RSTART + 2); |
||
| 283 | strike_done = nextil(strike); |
||
| 284 | return strike_t1 "<del>" strike_done "</del>" nextil(strike_rest); |
||
| 285 | } |
||
| 286 | return t1 "~~" nextil(t2); |
||
| 287 | } |
||
| 288 | return t1 "~" nextil(t2); |
||
| 289 | } |
||
| 290 | # Emphasis |
||
| 291 | if(match(tag, /[*_]/)){ |
||
| 292 | # An underscore between two word characters is literal, not an |
||
| 293 | # emphasis marker -- otherwise identifiers like node_modules and |
||
| 294 | # snake_case_name get torn apart, and an unmatched one leaves a |
||
| 295 | # stray <em> open for the rest of the document. CommonMark makes |
||
| 296 | # the same exception, and only for _: intraword * still emphasises. |
||
| 297 | if(tag == "_" && match(t1, /[A-Za-z0-9]$/) && match(t2, /^[A-Za-z0-9]/)) |
||
| 298 | return t1 tag nextil(t2); |
||
| 299 | # An underscore that is the whole content of an HTML element is |
||
| 300 | # literal: <span class="us">_</span> is a way of writing a visible |
||
| 301 | # underscore, not emphasis. Without this it opened an <em> that |
||
| 302 | # swallowed the markup after it, and two such underscores paired with |
||
| 303 | # each other across the tags between them, emitting mis-nested |
||
| 304 | # "<b><em></b> ... <b></em></b>". CommonMark reaches the same result |
||
| 305 | # by its flanking rules: a delimiter preceded by punctuation and |
||
| 306 | # followed by punctuation is neither opening nor closing. |
||
| 307 | if(tag == "_" && t1 == "" && match(t2, /^</) && stag[ns] != "_" && stag[ns] != "__") |
||
| 308 | return t1 tag nextil(t2); |
||
| 309 | ntag = tag; |
||
| 310 | # `tag` is a literal * or _, so it must be escaped before being |
||
| 311 | # interpolated into a regex: "^" "*" is not a valid regex. |
||
| 312 | etag = "^" re_quote(tag); |
||
| 313 | if(sub(etag, "", t2)){ |
||
| 314 | if(stag[ns] == tag && match(t2, etag)) |
||
| 315 | t2 = tag t2; |
||
| 316 | else |
||
| 317 | ntag = tag tag |
||
| 318 | } |
||
| 319 | n = length(ntag); |
||
| 320 | tag = (n == 2) ? "strong" : "em"; |
||
| 321 | if(match(t1, / $/) && match(t2, /^ /)) |
||
| 322 | return t1 tag nextil(t2); |
||
| 323 | if(stag[ns] == ntag){ |
||
| 324 | tag = "/" tag; |
||
| 325 | ns--; |
||
| 326 | } |
||
| 327 | else |
||
| 328 | stag[++ns] = ntag; |
||
| 329 | tag = "<" tag ">"; |
||
| 330 | return t1 tag nextil(t2); |
||
| 331 | } |
||
| 332 | } |
||
| 333 | |
||
| 334 | function inline(t) { |
||
| 335 | ilcode = 0; |
||
| 336 | ilcode2 = 0; |
||
| 337 | ns = 0; |
||
| 338 | |
||
| 339 | return nextil(t); |
||
| 340 | } |
||
| 341 | |
||
| 342 | # heading_id <text> |
||
| 343 | # The slug a heading is addressable by: lowercased, runs of anything that is |
||
| 344 | # not a letter or digit collapsed to a single dash, ends trimmed. |
||
| 345 | # |
||
| 346 | # A document long enough to link within needs its headings to be targets. Any |
||
| 347 | # "#section" link -- from its own table of contents, from a sibling page, from |
||
| 348 | # somewhere else entirely -- resolves to nothing without them. |
||
| 349 | # |
||
| 350 | # Inline markup is stripped first: a heading may be emphasised or hold code, |
||
| 351 | # and "<code>Vision</code>" must still slug to "vision". |
||
| 352 | # |
||
| 353 | # Collisions are left alone. Two headings with the same words produce the same |
||
| 354 | # id and the first wins, which is what a reader following a link expects and |
||
| 355 | # what every other implementation does; numbering them would make the ids |
||
| 356 | # depend on how many times a word appears earlier in the document. |
||
| 357 | function heading_id(t, s) { |
||
| 358 | s = t |
||
| 359 | gsub(/<[^>]*>/, "", s) |
||
| 360 | s = tolower(s) |
||
| 361 | gsub(/&[a-z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/, "", s) |
||
| 362 | gsub(/[^a-z0-9]+/, "-", s) |
||
| 363 | sub(/^-+/, "", s) |
||
| 364 | sub(/-+$/, "", s) |
||
| 365 | return s |
||
| 366 | } |
||
| 367 | |
||
| 368 | # para_id <html> |
||
| 369 | # The anchor a paragraph is addressable by: "p-" and a hash of the paragraph's |
||
| 370 | # own text. |
||
| 371 | # |
||
| 372 | # Derived from the content rather than the position, so a permalink survives |
||
| 373 | # edits elsewhere on the page -- insert three paragraphs above this one, or |
||
| 374 | # reorder the sections around it, and the link still resolves. It breaks only |
||
| 375 | # when this paragraph's own wording changes, which is the honest failure: the |
||
| 376 | # text someone linked to is not there any more. A positional p-7 would keep |
||
| 377 | # resolving after an insertion and quietly point at the wrong paragraph, which |
||
| 378 | # is worse than breaking. |
||
| 379 | # |
||
| 380 | # Inline markup is stripped first so that editing a link's target, or wrapping |
||
| 381 | # a word in emphasis, does not move the anchor. |
||
| 382 | function para_id(t, s) { |
||
| 383 | s = t |
||
| 384 | gsub(/<[^>]*>/, "", s) |
||
| 385 | gsub(/&[a-z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/, "", s) |
||
| 386 | gsub(/[ \t]+/, " ", s) |
||
| 387 | sub(/^ /, "", s); sub(/ $/, "", s) |
||
| 388 | if (s == "") return "" |
||
| 389 | return "p-" fnv1a(s) |
||
| 390 | } |
||
| 391 | |
||
| 392 | # fnv1a <string> |
||
| 393 | # FNV-1a, 32 bits, in awk rather than by calling out to a hash program: a page |
||
| 394 | # has hundreds of paragraphs and a sub-process each would be the slowest part |
||
| 395 | # of the build. Checked against every paragraph in the site at the time it was |
||
| 396 | # written -- no collisions between different texts. |
||
| 397 | function fnv1a(s, h, i, c, n) { |
||
| 398 | h = 2166136261 |
||
| 399 | n = length(s) |
||
| 400 | for (i = 1; i <= n; i++) { |
||
| 401 | c = index(HASHCHARS, substr(s, i, 1)) |
||
| 402 | if (c == 0) c = 32 |
||
| 403 | h = xor32(h, c) |
||
| 404 | h = (h * 16777619) % 4294967296 |
||
| 405 | } |
||
| 406 | return sprintf("%08x", h) |
||
| 407 | } |
||
| 408 | |
||
| 409 | function xor32(a, b, r, i, x, y) { |
||
| 410 | r = 0; i = 1 |
||
| 411 | while (a > 0 || b > 0) { |
||
| 412 | x = a % 2; y = b % 2 |
||
| 413 | if (x != y) r += i |
||
| 414 | a = int(a / 2); b = int(b / 2); i *= 2 |
||
| 415 | } |
||
| 416 | return r |
||
| 417 | } |
||
| 418 | |
||
| 419 | function printp(tag, id) { |
||
| 420 | if(!match(text, /^[ ]*$/)){ |
||
| 421 | text = inline(text); |
||
| 422 | if(tag != ""){ |
||
| 423 | # Headings carry an id; nothing else does. |
||
| 424 | if(tag ~ /^h[1-6]$/){ |
||
| 425 | id = heading_id(text) |
||
| 426 | if(id != "") |
||
| 427 | oprint("<" tag " id=\"" id "\">" text "</" tag ">"); |
||
| 428 | else |
||
| 429 | oprint("<" tag ">" text "</" tag ">"); |
||
| 430 | } |
||
| 431 | else if(tag == "p"){ |
||
| 432 | # Two paragraphs with the same words hash the same; the second |
||
| 433 | # and later take a suffix so every id on the page is unique. |
||
| 434 | id = para_id(text) |
||
| 435 | if(id != ""){ |
||
| 436 | PARA_SEEN[id]++ |
||
| 437 | if(PARA_SEEN[id] > 1) |
||
| 438 | id = id "-" PARA_SEEN[id] |
||
| 439 | oprint("<p id=\"" id "\">" text "</p>"); |
||
| 440 | } |
||
| 441 | else |
||
| 442 | oprint("<p>" text "</p>"); |
||
| 443 | } |
||
| 444 | else |
||
| 445 | oprint("<" tag ">" text "</" tag ">"); |
||
| 446 | } |
||
| 447 | else |
||
| 448 | oprint(text); |
||
| 449 | } |
||
| 450 | text = ""; |
||
| 451 | } |
||
| 452 | |
||
| 453 | BEGIN { |
||
| 454 | # Character table for fnv1a: index() gives each byte a value, so the hash |
||
| 455 | # needs no ord() -- which one-true-awk does not have. |
||
| 456 | HASHCHARS = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" |
||
| 457 | blank = 0; |
||
| 458 | code = 0; |
||
| 459 | hr = 0; |
||
| 460 | html = 0; |
||
| 461 | nl = 0; |
||
| 462 | nr = 0; |
||
| 463 | otext = ""; |
||
| 464 | text = ""; |
||
| 465 | par = "p"; |
||
| 466 | } |
||
| 467 | |
||
| 468 | # References |
||
| 469 | # Reference link definitions: [id]: url "optional title" |
||
| 470 | # Standard markdown. The [^!id] form below predates this and is kept so any |
||
| 471 | # document using it still resolves. Labels are case-insensitive, so they are |
||
| 472 | # normalised to lowercase on both definition and use. |
||
| 473 | !code && !fence && /^ *\[[^\^#\]][^\]]*\]:[ ]+/ { |
||
| 474 | sub(/^ *\[/, ""); |
||
| 475 | match($0, /\]/); |
||
| 476 | id = tolower(substr($0, 1, RSTART - 1)); |
||
| 477 | sub(/^[^\]]*\]:[ ]+/, ""); |
||
| 478 | title = ""; |
||
| 479 | if(match($0, /".*"$/)) |
||
| 480 | title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2); |
||
| 481 | sub(/[ ]+".*"$/, ""); |
||
| 482 | url = eschtml($0); |
||
| 483 | ref[id] = url title; |
||
| 484 | |
||
| 485 | subref(id); |
||
| 486 | next; |
||
| 487 | } |
||
| 488 | |
||
| 489 | !code && /^ *\[\^![^\]]*\]:[ ]+/ { |
||
| 490 | sub(/^ *\[\^!/, ""); |
||
| 491 | match($0, /\]/); |
||
| 492 | id = substr($0, 1, RSTART - 1); |
||
| 493 | sub(id "\\]:[ ]+", ""); |
||
| 494 | id = tolower(id); |
||
| 495 | title = ""; |
||
| 496 | if(match($0, /".*"$/)) |
||
| 497 | title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2); |
||
| 498 | sub(/[ ]+".*"$/, ""); |
||
| 499 | url = eschtml($0); |
||
| 500 | ref[id] = url title; |
||
| 501 | |
||
| 502 | subref(id); |
||
| 503 | next; |
||
| 504 | } |
||
| 505 | |
||
| 506 | !code && !fence && /^ *\[#[^\]]*\]:[ ]+/ { |
||
| 507 | sub(/^ *\[#/, ""); |
||
| 508 | match($0, /\]/); |
||
| 509 | id = substr($0, 1, RSTART - 1); |
||
| 510 | sub(/^[^\]]*\]:[ ]+/, ""); |
||
| 511 | cite_body[id] = $0; |
||
| 512 | next; |
||
| 513 | } |
||
| 514 | |
||
| 515 | !code && /^ *\[\^[^\]]*\]:[ ]+/ { |
||
| 516 | sub(/^ *\[\^/, ""); |
||
| 517 | match($0, /\]/); |
||
| 518 | id = substr($0, 1, RSTART - 1); |
||
| 519 | sub(id "\\]:[ ]+", ""); |
||
| 520 | sub(/[ ]+".*"$/, ""); |
||
| 521 | # Stored raw, like a citation's body: inline() escapes what needs it and |
||
| 522 | # resolves what does not. Pre-escaping turned every "<" into "<" before |
||
| 523 | # the parser could see it, so an autolink -- <https://example.com> -- came |
||
| 524 | # out as literal text in a footnote while working in prose and in |
||
| 525 | # citations. The variable was called "url" because this rule began life |
||
| 526 | # handling link-reference definitions, where a bare URL was all it held. |
||
| 527 | fnref[id] = $0; |
||
| 528 | |
||
| 529 | subref(id); |
||
| 530 | next; |
||
| 531 | } |
||
| 532 | |
||
| 533 | # html |
||
| 534 | !html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ |
||
| 535 | isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ { |
||
| 536 | if(code) |
||
| 537 | oprint(code_held "</code></pre>"); code_held = ""; |
||
| 538 | for(; !text && block[nl] == "blockquote"; nl--) |
||
| 539 | oprint("</blockquote>"); |
||
| 540 | match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ |
||
| 541 | isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/); |
||
| 542 | htag = substr($0, 2, RLENGTH - 1); |
||
| 543 | if(!match($0, "(<\\/" htag ">)|((^<hr ?\\/?)|(--)>$)")) |
||
| 544 | html = 1; |
||
| 545 | if(html && match($0, /^<hr/)) |
||
| 546 | hr = 1; |
||
| 547 | oprint($0); |
||
| 548 | next; |
||
| 549 | } |
||
| 550 | |
||
| 551 | html && (/(^<\/(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ |
||
| 552 | isindex|menu|noframes|noscript|ol|p|pre|table|ul).*)|(--)>$/ || |
||
| 553 | (hr && />$/)) { |
||
| 554 | html = 0; |
||
| 555 | hr = 0; |
||
| 556 | oprint($0); |
||
| 557 | next; |
||
| 558 | } |
||
| 559 | |
||
| 560 | html { |
||
| 561 | oprint($0); |
||
| 562 | next; |
||
| 563 | } |
||
| 564 | |
||
| 565 | # MultiMarkdown metadata block |
||
| 566 | # |
||
| 567 | # Key: value pairs at the very top of the file, ending at the first blank |
||
| 568 | # line. The pairs are preserved verbatim rather than stripped -- the author |
||
| 569 | # wrote them deliberately -- but each is wrapped so a theme can style or hide |
||
| 570 | # it, and so jenny can read the values back. Whether they are visible is a |
||
| 571 | # readability decision for the layout, not the parser's to make. |
||
| 572 | # |
||
| 573 | # The entire leading run must look like metadata. If any line before the first |
||
| 574 | # blank is not Key: value, the whole block is ordinary prose -- otherwise a |
||
| 575 | # post opening "Note: something" would silently become metadata. |
||
| 576 | # |
||
| 577 | # Lines are buffered rather than consumed with getline, so the main rules see |
||
| 578 | # them unchanged when the block turns out to be prose. |
||
| 579 | meta_state == 0 { |
||
| 580 | if(NR == 1 && $0 ~ /^[A-Za-z][A-Za-z0-9 _-]*:[ \t]*./){ |
||
| 581 | meta_state = 1; |
||
| 582 | meta_n = 0; |
||
| 583 | meta_buf[++meta_n] = $0; |
||
| 584 | next; |
||
| 585 | } |
||
| 586 | meta_state = 2; |
||
| 587 | } |
||
| 588 | |
||
| 589 | meta_state == 1 { |
||
| 590 | if($0 ~ /^[ \t]*$/){ |
||
| 591 | # End of the block, and every line matched: emit them as metadata. |
||
| 592 | for(mi = 1; mi <= meta_n; mi++){ |
||
| 593 | mline = meta_buf[mi]; |
||
| 594 | match(mline, /:/); |
||
| 595 | mkey = substr(mline, 1, RSTART - 1); |
||
| 596 | mval = substr(mline, RSTART + 1); |
||
| 597 | sub(/^[ \t]+/, "", mval); |
||
| 598 | sub(/[ \t]+$/, "", mval); |
||
| 599 | oprint("<p class=\"meta\" data-key=\"" escattr(mkey) \ |
||
| 600 | "\" data-value=\"" escattr(mval) "\">" eschtml(mline) "</p>"); |
||
| 601 | } |
||
| 602 | meta_state = 2; |
||
| 603 | next; |
||
| 604 | } |
||
| 605 | if($0 ~ /^[A-Za-z][A-Za-z0-9 _-]*:[ \t]*./){ |
||
| 606 | meta_buf[++meta_n] = $0; |
||
| 607 | next; |
||
| 608 | } |
||
| 609 | # Not a metadata block after all. The buffered lines are ordinary prose, |
||
| 610 | # and awk cannot re-run its own rules over them, so they are pushed back |
||
| 611 | # into the paragraph accumulator that the fall-through rule uses. That is |
||
| 612 | # where a run of plain lines would have ended up anyway. |
||
| 613 | meta_state = 2; |
||
| 614 | for(mi = 1; mi <= meta_n; mi++) |
||
| 615 | text = (text ? text " " : "") eschtml(meta_buf[mi]); |
||
| 616 | meta_n = 0; |
||
| 617 | } |
||
| 618 | |
||
| 619 | # Tables (MultiMarkdown) |
||
| 620 | # |
||
| 621 | # A row is only a table row if the line after it is a separator: |---|:-:|. |
||
| 622 | # That means one line of lookahead, which is done by holding the candidate |
||
| 623 | # header until the next line decides. Alignment comes from the colons in the |
||
| 624 | # separator and is emitted as an inline style, matching multimarkdown's own |
||
| 625 | # output. |
||
| 626 | |
||
| 627 | function table_cells(line, n, i, out) { |
||
| 628 | # Split on unescaped pipes, dropping the optional edge pipes. |
||
| 629 | sub(/^[ \t]*\|/, "", line); |
||
| 630 | sub(/\|[ \t]*$/, "", line); |
||
| 631 | return line; |
||
| 632 | } |
||
| 633 | |
||
| 634 | function table_align(spec, a) { |
||
| 635 | a = spec; |
||
| 636 | gsub(/[ \t]/, "", a); |
||
| 637 | if(a ~ /^:.*:$/) return "center"; |
||
| 638 | if(a ~ /:$/) return "right"; |
||
| 639 | if(a ~ /^:/) return "left"; |
||
| 640 | return ""; |
||
| 641 | } |
||
| 642 | |
||
| 643 | function table_row(line, celltag, n, i, cells, out, st) { |
||
| 644 | n = split(table_cells(line), cells, "|"); |
||
| 645 | out = "<tr>"; |
||
| 646 | for(i = 1; i <= n; i++){ |
||
| 647 | sub(/^[ \t]+/, "", cells[i]); |
||
| 648 | sub(/[ \t]+$/, "", cells[i]); |
||
| 649 | st = ""; |
||
| 650 | if(tbl_align[i] != "") |
||
| 651 | st = " style=\"text-align:" tbl_align[i] ";\""; |
||
| 652 | out = out "\n\t<" celltag st ">" inline(cells[i]) "</" celltag ">"; |
||
| 653 | } |
||
| 654 | return out "\n</tr>"; |
||
| 655 | } |
||
| 656 | |
||
| 657 | # A held header row, resolved by whatever follows it. |
||
| 658 | tbl_pending != "" { |
||
| 659 | if($0 ~ /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/){ |
||
| 660 | # It was a table after all. |
||
| 661 | n_align = split(table_cells($0), align_spec, "|"); |
||
| 662 | for(ai = 1; ai <= n_align; ai++) |
||
| 663 | tbl_align[ai] = table_align(align_spec[ai]); |
||
| 664 | if(text) |
||
| 665 | printp(par ? par : "p"); |
||
| 666 | oprint("<div class=\"table-scroll\">"); |
||
| 667 | oprint("<table>"); |
||
| 668 | oprint("<thead>"); |
||
| 669 | oprint(table_row(tbl_pending, "th")); |
||
| 670 | oprint("</thead>"); |
||
| 671 | oprint("<tbody>"); |
||
| 672 | tbl = 1; |
||
| 673 | tbl_pending = ""; |
||
| 674 | next; |
||
| 675 | } |
||
| 676 | # Not a table: the held line is ordinary text. |
||
| 677 | text = (text ? text " " : "") tbl_pending; |
||
| 678 | tbl_pending = ""; |
||
| 679 | } |
||
| 680 | |
||
| 681 | tbl { |
||
| 682 | if($0 ~ /^[ \t]*$/ || $0 !~ /\|/){ |
||
| 683 | oprint("</tbody>"); |
||
| 684 | oprint("</table>"); |
||
| 685 | oprint("</div>"); |
||
| 686 | tbl = 0; |
||
| 687 | for(ci in tbl_align) delete tbl_align[ci]; |
||
| 688 | if($0 ~ /^[ \t]*$/) |
||
| 689 | next; |
||
| 690 | } |
||
| 691 | else{ |
||
| 692 | oprint(table_row($0, "td")); |
||
| 693 | next; |
||
| 694 | } |
||
| 695 | } |
||
| 696 | |
||
| 697 | !fence && !code && !tbl && /\|/ && !/^[ \t]*$/ && (!text || text == "") { |
||
| 698 | # Candidate header row: hold it and let the next line decide. Only a line |
||
| 699 | # whose pipes separate non-empty cells qualifies, so ordinary prose that |
||
| 700 | # happens to contain a pipe is not swallowed. |
||
| 701 | tbl_probe = $0; |
||
| 702 | sub(/^[ \t]*\|/, "", tbl_probe); |
||
| 703 | sub(/\|[ \t]*$/, "", tbl_probe); |
||
| 704 | if(index(tbl_probe, "|") > 0 || $0 ~ /^[ \t]*\|.*\|[ \t]*$/){ |
||
| 705 | tbl_pending = $0; |
||
| 706 | next; |
||
| 707 | } |
||
| 708 | } |
||
| 709 | |
||
| 710 | # Fenced code blocks (MultiMarkdown / CommonMark) |
||
| 711 | # |
||
| 712 | # Must run before every other line rule: content inside a fence is literal, |
||
| 713 | # so it has to bypass inline parsing, smart quotes and list detection alike. |
||
| 714 | # The closing fence has to be at least as long as the opener and use the same |
||
| 715 | # character, so a fence can contain a shorter run of backticks. |
||
| 716 | fence { |
||
| 717 | if($0 ~ fence_re && length($0) >= 0){ |
||
| 718 | # Candidate closer: same char, at least as long, nothing after it. |
||
| 719 | if(match($0, "^[ \t]*" fence_char "{" fence_len ",}[ \t]*$")){ |
||
| 720 | if(fence_first) |
||
| 721 | # An empty fence: nothing was ever buffered. |
||
| 722 | oprint(fence_open fence_close); |
||
| 723 | else |
||
| 724 | oprint(fence_held fence_close); |
||
| 725 | fence = 0; |
||
| 726 | fence_held = ""; |
||
| 727 | next; |
||
| 728 | } |
||
| 729 | } |
||
| 730 | # Hold each line until the next one arrives, so the closing tag can be |
||
| 731 | # joined to the last line rather than sitting on its own. |
||
| 732 | if(fence_first){ |
||
| 733 | fence_held = fence_open (fence_raw ? $0 : escfence($0)); |
||
| 734 | fence_first = 0; |
||
| 735 | } |
||
| 736 | else{ |
||
| 737 | oprint(fence_held); |
||
| 738 | fence_held = (fence_raw ? $0 : escfence($0)); |
||
| 739 | } |
||
| 740 | next; |
||
| 741 | } |
||
| 742 | |
||
| 743 | /^[ \t]*(```|~~~)/ { |
||
| 744 | # Opening fence. Everything after the run is an info string; its first |
||
| 745 | # word is the language. |
||
| 746 | line = $0; |
||
| 747 | sub(/^[ \t]*/, "", line); |
||
| 748 | fence_char = substr(line, 1, 1); |
||
| 749 | fence_len = 0; |
||
| 750 | while(substr(line, fence_len + 1, 1) == fence_char) |
||
| 751 | fence_len++; |
||
| 752 | info = substr(line, fence_len + 1); |
||
| 753 | sub(/^[ \t]+/, "", info); |
||
| 754 | sub(/[ \t].*$/, "", info); |
||
| 755 | |
||
| 756 | # Close anything currently open so the block is not nested inside a <p>. |
||
| 757 | if(text) |
||
| 758 | printp(par ? par : "p"); |
||
| 759 | if(code){ |
||
| 760 | oprint(code_held "</code></pre>"); code_held = ""; |
||
| 761 | code = 0; |
||
| 762 | } |
||
| 763 | |
||
| 764 | fence = 1; |
||
| 765 | fence_first = 1; |
||
| 766 | fence_re = "^[ \t]*" fence_char; |
||
| 767 | # A diagram language is held for a client-side renderer rather than shown |
||
| 768 | # as source. The text goes in verbatim: mermaid reads the element's text |
||
| 769 | # content and parses it itself, so escaping "-->" to "-->" would break |
||
| 770 | # every arrow in the diagram. That makes this the one place a fence's |
||
| 771 | # content is not escaped, which is safe only because the set of languages |
||
| 772 | # treated this way is fixed here -- an author cannot introduce a new one |
||
| 773 | # from a document. |
||
| 774 | # |
||
| 775 | # The language is kept as a class so a theme decides what to do with it. |
||
| 776 | # jenny renders no diagrams itself and loads nothing; a theme that wants |
||
| 777 | # them supplies the renderer. |
||
| 778 | fence_raw = 0; |
||
| 779 | fence_close = "</code></pre>"; |
||
| 780 | lang = info; |
||
| 781 | sub(/[ \t].*$/, "", lang); |
||
| 782 | if(lang == "mermaid" || lang == "mermaid-legend"){ |
||
| 783 | fence_raw = 1; |
||
| 784 | fence_close = "</div>"; |
||
| 785 | fence_open = "<div class=\"diagram diagram-" eschtml(lang) "\">"; |
||
| 786 | } |
||
| 787 | else if(info != "") |
||
| 788 | fence_open = "<pre><code class=\"language-" eschtml(info) "\">"; |
||
| 789 | else |
||
| 790 | fence_open = "<pre><code>"; |
||
| 791 | next; |
||
| 792 | } |
||
| 793 | |
||
| 794 | # List and quote blocks |
||
| 795 | |
||
| 796 | # Remove indentation |
||
| 797 | # |
||
| 798 | # One nesting level is peeled per step. A tab or four spaces was the only unit |
||
| 799 | # understood here, but a child is conventionally indented to its parent's |
||
| 800 | # content column, which for "- " is two spaces -- so two-space nesting, the |
||
| 801 | # common form, was invisible: the item regex below tolerates up to three |
||
| 802 | # leading spaces as noise, so a two-space child matched as a sibling and the |
||
| 803 | # whole list came out flat. |
||
| 804 | # |
||
| 805 | # Two spaces is the step, not an alternative to four. Written as an |
||
| 806 | # alternation with four spaces first, a four-space indent matched the wide |
||
| 807 | # branch and peeled a single level, so three levels of four-space nesting |
||
| 808 | # collapsed into two. A tab remains one whole level. |
||
| 809 | { |
||
| 810 | for(nnl = 0; nnl < nl; nnl++) |
||
| 811 | if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \ |
||
| 812 | (block[nnl + 1] == "blockquote" && !sub(/^> ?/, ""))) |
||
| 813 | break; |
||
| 814 | } |
||
| 815 | nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; } |
||
| 816 | # Quote blocks |
||
| 817 | { |
||
| 818 | while(sub(/^> /, "")) |
||
| 819 | nblock[++nnl] = "blockquote"; |
||
| 820 | } |
||
| 821 | # Horizontal rules |
||
| 822 | { hr = 0; } |
||
| 823 | (blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ { |
||
| 824 | if(code){ |
||
| 825 | oprint(code_held "</code></pre>"); code_held = ""; |
||
| 826 | code = 0; |
||
| 827 | } |
||
| 828 | blank = 0; |
||
| 829 | nnl = 0; |
||
| 830 | hr = 1; |
||
| 831 | } |
||
| 832 | # List items |
||
| 833 | block[nl] ~ /[ou]l/ && /^$/ { |
||
| 834 | blank = 1; |
||
| 835 | next; |
||
| 836 | } |
||
| 837 | { newli = 0; } |
||
| 838 | !hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ { |
||
| 839 | sub(/^ ? ? ?[*+-]( +| )/, ""); |
||
| 840 | nnl++; |
||
| 841 | nblock[nnl] = "ul"; |
||
| 842 | newli = 1; |
||
| 843 | } |
||
| 844 | (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ { |
||
| 845 | sub(/^ ? ? ?([0-9]+\.)+( +| )/, ""); |
||
| 846 | nnl++; |
||
| 847 | nblock[nnl] = "ol"; |
||
| 848 | newli = 1; |
||
| 849 | } |
||
| 850 | newli { |
||
| 851 | if(blank && nnl == nl && !par) |
||
| 852 | par = "p"; |
||
| 853 | blank = 0; |
||
| 854 | printp(par); |
||
| 855 | if(nnl == nl && block[nl] == nblock[nl]) |
||
| 856 | oprint("</li><li>"); |
||
| 857 | } |
||
| 858 | blank && ! /^$/ { |
||
| 859 | if(match(block[nnl], /[ou]l/) && !par) |
||
| 860 | par = "p"; |
||
| 861 | printp(par); |
||
| 862 | par = "p"; |
||
| 863 | blank = 0; |
||
| 864 | } |
||
| 865 | |
||
| 866 | # Close old blocks and open new ones |
||
| 867 | nnl != nl || nblock[nl] != block[nl] { |
||
| 868 | if(code){ |
||
| 869 | oprint(code_held "</code></pre>"); code_held = ""; |
||
| 870 | code = 0; |
||
| 871 | } |
||
| 872 | printp(par); |
||
| 873 | b = (nnl > nl) ? nblock[nnl] : block[nnl]; |
||
| 874 | par = (match(b, /[ou]l/)) ? "" : "p"; |
||
| 875 | } |
||
| 876 | nnl < nl || (nnl == nl && nblock[nl] != block[nl]) { |
||
| 877 | # The second clause compares against nblock, the block kind this line |
||
| 878 | # wants, not pblock -- which is assigned nowhere and so was always empty, |
||
| 879 | # making the test true for any open list. Once the walk reached nnl it |
||
| 880 | # therefore kept going and closed the outer <ul> along with the nested one, |
||
| 881 | # leaving a dedented sibling no list to rejoin. Harmless while nesting did |
||
| 882 | # not work at all; load-bearing now that it does. |
||
| 883 | for(; nl > nnl || (nl > 0 && nnl == nl && nblock[nl] != block[nl]); nl--){ |
||
| 884 | if(match(block[nl], /[ou]l/)) |
||
| 885 | oprint("</li>"); |
||
| 886 | oprint("</" block[nl] ">"); |
||
| 887 | } |
||
| 888 | # A sibling after a nested list needs a fresh <li> in the list we just came |
||
| 889 | # back to. The "</li><li>" rule earlier only fires when the item is already |
||
| 890 | # at its own depth (nnl == nl), which is false on a dedent. |
||
| 891 | if(newli && nnl > 0 && nnl == nl && match(block[nl], /[ou]l/) && block[nl] == nblock[nl]){ |
||
| 892 | oprint("</li><li>"); |
||
| 893 | newli = 0; |
||
| 894 | } |
||
| 895 | } |
||
| 896 | nnl > nl { |
||
| 897 | for(; nl < nnl; nl++){ |
||
| 898 | block[nl + 1] = nblock[nl + 1]; |
||
| 899 | oprint("<" block[nl + 1] ">"); |
||
| 900 | if(match(block[nl + 1], /[ou]l/)) |
||
| 901 | oprint("<li>"); |
||
| 902 | } |
||
| 903 | } |
||
| 904 | hr { |
||
| 905 | oprint("<hr>"); |
||
| 906 | next; |
||
| 907 | } |
||
| 908 | |
||
| 909 | # Code blocks |
||
| 910 | code && /^$/ { |
||
| 911 | if(blanK) |
||
| 912 | oprint(""); |
||
| 913 | blank = 1; |
||
| 914 | next; |
||
| 915 | } |
||
| 916 | !text && sub(/^( | )/, "") { |
||
| 917 | if(blanK) |
||
| 918 | oprint(""); |
||
| 919 | blank = 0; |
||
| 920 | $0 = eschtml($0); |
||
| 921 | if(!code){ |
||
| 922 | code_held = "<pre><code>" $0; |
||
| 923 | code = 1; |
||
| 924 | } |
||
| 925 | else{ |
||
| 926 | oprint(code_held); |
||
| 927 | code_held = $0; |
||
| 928 | } |
||
| 929 | next; |
||
| 930 | } |
||
| 931 | code { |
||
| 932 | oprint(code_held "</code></pre>"); |
||
| 933 | code_held = ""; |
||
| 934 | code = 0; |
||
| 935 | } |
||
| 936 | |
||
| 937 | # Setex-style Headers |
||
| 938 | text && /^=+$/ {printp("h1"); next;} |
||
| 939 | text && /^-+$/ {printp("h2"); next;} |
||
| 940 | |
||
| 941 | # Atx-Style headers |
||
| 942 | /^#+/ && (!newli || par=="p" || /^##/) { |
||
| 943 | for(n = 0; n < 6 && sub(/^# */, ""); n++) |
||
| 944 | sub(/#$/, ""); |
||
| 945 | par = "h" n; |
||
| 946 | } |
||
| 947 | |
||
| 948 | # Paragraph |
||
| 949 | /^$/ { |
||
| 950 | printp(par); |
||
| 951 | par = "p"; |
||
| 952 | next; |
||
| 953 | } |
||
| 954 | |
||
| 955 | # Add text |
||
| 956 | { text = (text ? text " " : "") $0; } |
||
| 957 | |
||
| 958 | function alen(a, ix, k) { |
||
| 959 | k = 0 |
||
| 960 | for(ix in a) k++ |
||
| 961 | return k |
||
| 962 | } |
||
| 963 | |
||
| 964 | END { |
||
| 965 | # A candidate header row with no separator after it was ordinary text. |
||
| 966 | if(tbl_pending != ""){ |
||
| 967 | text = (text ? text " " : "") tbl_pending; |
||
| 968 | tbl_pending = ""; |
||
| 969 | } |
||
| 970 | if(tbl){ |
||
| 971 | oprint("</tbody>"); |
||
| 972 | oprint("</table>"); |
||
| 973 | oprint("</div>"); |
||
| 974 | tbl = 0; |
||
| 975 | } |
||
| 976 | if(code){ |
||
| 977 | oprint(code_held "</code></pre>"); code_held = ""; |
||
| 978 | code = 0; |
||
| 979 | } |
||
| 980 | printp(par); |
||
| 981 | for(; nl > 0; nl--){ |
||
| 982 | if(match(block[nl], /[ou]l/)) |
||
| 983 | oprint("</li>"); |
||
| 984 | oprint("</" block[nl] ">"); |
||
| 985 | } |
||
| 986 | # A reference whose definition never appeared is not a link at all. |
||
| 987 | # Restore the literal text the author wrote rather than emitting an |
||
| 988 | # anchor with an empty href. |
||
| 989 | for(sid in shortcut_ref){ |
||
| 990 | lt = shortcut_ref[sid]; |
||
| 991 | gsub("<a href=\"<<" sid "\">" lt "</a>", "[" lt "]", otext); |
||
| 992 | } |
||
| 993 | while(match(otext, /<a href="<<[^"]*">[^<]*<\/a>/)){ |
||
| 994 | frag = substr(otext, RSTART, RLENGTH); |
||
| 995 | lt = frag; |
||
| 996 | sub(/^<a href="<<[^"]*">/, "", lt); |
||
| 997 | sub(/<\/a>$/, "", lt); |
||
| 998 | rid = frag; |
||
| 999 | sub(/^<a href="<</, "", rid); |
||
| 1000 | sub(/">.*$/, "", rid); |
||
| 1001 | otext = substr(otext, 1, RSTART - 1) "[" lt "][" rid "]" \ |
||
| 1002 | substr(otext, RSTART + RLENGTH); |
||
| 1003 | } |
||
| 1004 | gsub(/<<[^"]*/, "", otext); |
||
| 1005 | print(otext); |
||
| 1006 | |
||
| 1007 | # Print citations, in order of first use. |
||
| 1008 | if(cite_n > 0) { |
||
| 1009 | print "<div class=\"citations\">"; |
||
| 1010 | print "<hr />"; |
||
| 1011 | print "<ol>"; |
||
| 1012 | for(ci = 1; ci <= cite_n; ci++){ |
||
| 1013 | cid = cite_order[ci]; |
||
| 1014 | print "<li id=\"cn:" ci "\"><p>" inline(cite_body[cid]) \ |
||
| 1015 | " <a href=\"#cnref:" ci "\" title=\"return to body\" class=\"reversecitation\"> ↩︎</a></p></li>"; |
||
| 1016 | } |
||
| 1017 | print "</ol>"; |
||
| 1018 | print "</div>"; |
||
| 1019 | } |
||
| 1020 | |
||
| 1021 | # Print footnotes |
||
| 1022 | if(alen(fnref)>0) { |
||
| 1023 | print "<ul class=\"fn-list\">"; |
||
| 1024 | for (i in fnref) print "<li id=\"fn-" i "\" class=\"fn-item\"><span class=\"fn-handle\">" i ": </span><span class=\"fn-text\">" inline(fnref[i]) " <a href=\"#fnref-" i "\" class=\"fn-backref\">↩︎</a></span></li>"; |
||
| 1025 | print "</ul>"; |
||
| 1026 | } |
||
| 1027 | } |
||
test/regressions.sh +1 -1
suite "nested lists" | |||
| 767 | # two spaces for "- ". Two-space nesting, the common form, came out flat. |
767 | # two spaces for "- ". Two-space nesting, the common form, came out flat. |
| 768 | |
768 | |
| 769 | md_render() { |
769 | md_render() { |
| 770 | printf '%s\n' "$1" | "$AWK" -f "$JENNY_SHARE_DIR/lib/md2html.awk" |
||
| 770 | printf '%s\n' "$1" | "$AWK" -f "$JENNY_SHARE_DIR/lib/markdown.awk/markdown.awk" |
||
| 771 | } |
771 | } |
| 772 | |
772 | |
| 773 | it "a two-space child nests inside its parent" |
773 | it "a two-space child nests inside its parent" |
test/units.sh +7 -1
URL_STYLE="" | |||
| 306 | |
306 | |
| 307 | suite "markdown: intraword underscores" |
307 | suite "markdown: intraword underscores" |
| 308 | |
308 | |
| 309 | md() { printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/share/jenny/lib/md2html.awk"; } |
||
| 309 | # The deep-link id is stripped so these assertions can state the markup they |
||
| 310 | # are about. Each is checking how a span of text is parsed, not that a |
||
| 311 | # paragraph carries an anchor -- that has tests of its own, and pinning a |
||
| 312 | # content hash in every one of these would make them unreadable and would |
||
| 313 | # break them all again on any wording change. |
||
| 314 | md() { printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/share/jenny/lib/markdown.awk/markdown.awk" \ |
||
| 315 | | "$SED" 's/<p id="p-[0-9a-f]*\(-[0-9]*\)\{0,1\}">/<p>/g'; } |
||
| 310 | |
316 | |
| 311 | it "an underscore inside a word is literal" |
317 | it "an underscore inside a word is literal" |
| 312 | assert_eq "$(md 'node_modules here')" "<p>node_modules here</p>" |
318 | assert_eq "$(md 'node_modules here')" "<p>node_modules here</p>" |
Raw
commit 5479d3a7c6e2e184513c3da317164688f476d6db
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date: Sat Sep 5 10:06:44 2026 +0800
Take the markdown parser as a submodule instead of vendoring it
The parser had no dependency on the generator it lived in -- the only two
mentions of jenny were comments standing in for "whatever consumes this".
It is its own repository now, with its own tests, and comes back in at
share/jenny/lib/markdown.awk.
The recorded URL is the anonymous git:// one so that cloning this repo
works without an account on the host; the push URL is set to ssh locally.
The build signature had to change with it. It hashed the lib directory
with -maxdepth 1, and the parser is one directory down now, so a parser
edit stopped moving the signature -- verified before fixing: the hash was
identical across a change to the parser, which is the silent
skip-everything the depth-limited scan was originally added to prevent,
arriving by a different route. The scan is unlimited now and excludes the
submodule's own tests, which change without changing a rendered byte.
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e9d6076
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "share/jenny/lib/markdown.awk"]
+ path = share/jenny/lib/markdown.awk
+ url = git://grit.deadkey.org/mailmanpat/markdown.awk.git
diff --git a/bin/jenny b/bin/jenny
index 29df54f..4a5cadd 100755
--- a/bin/jenny
+++ b/bin/jenny
@@ -142,7 +142,7 @@ if source_blogrc; then
[ -z "${POSTS_PER_PAGE:-}" ] && POSTS_PER_PAGE=10
[ -z "${DIST:-}" ] && DIST=$PWD/.dist
[ -z "${LAYOUT_DIR:-}" ] && LAYOUT_DIR=$SHARE_DIR/layout
- [ -z "${MARKDOWN_COMMAND:-}" ] && MARKDOWN_COMMAND="\$AWK -f \$SHARE_DIR/lib/md2html.awk"
+ [ -z "${MARKDOWN_COMMAND:-}" ] && MARKDOWN_COMMAND="\$AWK -f \$SHARE_DIR/lib/markdown.awk/markdown.awk"
[ -z "${FEED_MAX_ITEMS:-}" ] && FEED_MAX_ITEMS=20
[ -z "${URL_STYLE:-}" ] && URL_STYLE=html
diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 54ea40a..9285d67 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -978,7 +978,7 @@ cat << _EOF_
.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }
ol, ul { padding-left: 1em; }
/* Notes and references are two different lists, so each is labelled.
- The wording lives here rather than in the parser: md2html.awk marks
+ The wording lives here rather than in the parser: markdown.awk marks
them with classes and leaves naming to the theme, which keeps
English out of the parser and lets a theme relabel or hide either
by overriding one rule. Styled to match an h5. */
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index dd2f46c..0830c02 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -799,7 +799,7 @@ cat << _EOF_
.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }
ol, ul { padding-left: 1em; }
/* Notes and references are two different lists, so each is labelled.
- The wording lives here rather than in the parser: md2html.awk marks
+ The wording lives here rather than in the parser: markdown.awk marks
them with classes and leaves naming to the theme, which keeps
English out of the parser and lets a theme relabel or hide either
by overriding one rule. Styled to match an h5. */
diff --git a/share/jenny/lib/helpers.sh b/share/jenny/lib/helpers.sh
index 6f32794..14ee52c 100644
--- a/share/jenny/lib/helpers.sh
+++ b/share/jenny/lib/helpers.sh
@@ -211,7 +211,16 @@ function build_signature() {
# surely as a template does: adding ids to headings changed the output of
# every page, and without this the lock skipped them all -- the build said
# nothing had changed while the change was in the markdown parser.
- find "${JENNY_LIB_DIR:-}" -maxdepth 1 -type f \( -name '*.sh' -o -name '*.awk' \) \
+ #
+ # No -maxdepth: the parser is a submodule now, one directory down, and a
+ # depth-limited scan stopped seeing it. Verified before the fix -- editing
+ # the parser left the signature identical, which is the same silent
+ # skip-everything this line was added to prevent, returned by a different
+ # route. The submodule's own tests are excluded: they change without
+ # changing a single rendered byte, and hashing them would rebuild the
+ # whole site for a test edit.
+ find "${JENNY_LIB_DIR:-}" -type f \( -name '*.sh' -o -name '*.awk' \) \
+ -not -path '*/test/*' \
-print0 2>/dev/null \
| LC_ALL=C sort -z \
| xargs -0 cat 2>/dev/null
@@ -848,7 +857,7 @@ function read_nav() {
#
# The parser owns what counts as metadata and wraps each pair in the rendered
# output. jenny reads the same block from the source to hydrate variables the
-# layout can use, so there is one definition of the syntax, in md2html.awk,
+# layout can use, so there is one definition of the syntax, in markdown.awk,
# and this stays a straight read of the same shape.
# get_metadata <file>
diff --git a/share/jenny/lib/markdown.awk b/share/jenny/lib/markdown.awk
new file mode 160000
index 0000000..8cd72df
--- /dev/null
+++ b/share/jenny/lib/markdown.awk
@@ -0,0 +1 @@
+Subproject commit 8cd72df31b3ae05a98735b419311da6274329401
diff --git a/share/jenny/lib/md2html.awk b/share/jenny/lib/md2html.awk
deleted file mode 100755
index f2ef676..0000000
--- a/share/jenny/lib/md2html.awk
+++ /dev/null
@@ -1,1027 +0,0 @@
-#!/bin/awk -f
-#
-# by: Jesus Galan (yiyus) 2009
-#
-# Usage: md2html.awk file.md > file.html
-# See: http://4l77.com/src/md2html.awk
-
-# Attribute values need quotes escaped as well as the usual entities.
-function escattr(t) {
- t = eschtml(t);
- gsub(">", "\\>", t);
- gsub("\"", "\\"", t);
- return t;
-}
-
-# Fenced content is fully literal, so > is escaped too. eschtml is shared with
-# inline parsing, where a bare > is legitimate text, so it is left alone.
-function escfence(t) {
- t = eschtml(t);
- gsub(">", "\\>", t);
- return t;
-}
-
-function eschtml(t) {
- gsub("&", "\\&", t);
- gsub("<", "\\<", t);
- return t;
-}
-
-function oprint(t){
- if(nr == 0)
- print t;
- else
- otext = otext "\n" t;
-}
-
-function subref(id){
- for(; nr > 0 && sub("<<" id, ref[id], otext); nr--);
- if(nr == 0 && otext) {
- print otext;
- otext = "";
- }
-}
-
-# Escapes regex metacharacters so a literal string can be interpolated into
-# a dynamic regex. Without this "^" "*" reaches the regex engine, which mawk
-# rejects outright and gawk quietly mismatches.
-function re_quote(s, out, i, c) {
- out = "";
- for(i = 1; i <= length(s); i++) {
- c = substr(s, i, 1);
- if(index("\\^$.[]|()*+?{}/", c) > 0)
- out = out "\\" c;
- else
- out = out c;
- }
- return out;
-}
-
-function nextil(t) {
- if(!match(t, /[`<&\[*_~\\-]|(!\[)|(\[\^)|(\[#)/))
- return t;
- t1 = substr(t, 1, RSTART - 1);
- tag = substr(t, RSTART, RLENGTH);
- t2 = substr(t, RSTART + RLENGTH);
- if(ilcode && tag != "`")
- return eschtml(t1 tag) nextil(t2);
- # Backslash escaping
- if(tag == "\\"){
- if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){
- tag = substr(t2, 1, 1);
- t2 = substr(t2, 2);
- }
- return t1 tag nextil(t2);
- }
- # Dashes
- if(tag == "-"){
- if(sub(/^-/, "", t2))
- tag = "—";
- return t1 tag nextil(t2);
- }
- # Inline Code
- if(tag == "`"){
- if(sub(/^`/, "", t2)){
- if(!match(t2, /``/))
- return t1 "”" nextil(t2);
- ilcode2 = !ilcode2;
- }
- else if(ilcode2)
- return t1 tag nextil(t2);
- tag = "<code>";
- if(ilcode){
- t1 = eschtml(t1);
- tag = "</code>";
- }
- ilcode = !ilcode;
- return t1 tag nextil(t2);
- }
- if(tag == "<"){
- # Autolinks
- if(match(t2, /^[^ ]+[\.@][^ ]+>/)){
- url = eschtml(substr(t2, 1, RLENGTH - 1));
- t2 = substr(t2, RLENGTH + 1);
- linktext = url;
- if(match(url, /@/) && !match(url, /^mailto:/))
- url = "mailto:" url;
- return t1 "<a href=\"" url "\">" linktext "</a>" nextil(t2);
- }
- # Html tags
- if(match(t2, /^[A-Za-z\/!][^>]*>/)){
- tag = tag substr(t2, RSTART, RLENGTH);
- t2 = substr(t2, RLENGTH + 1);
- return t1 tag nextil(t2);
- }
- return t1 "<" nextil(t2);
- }
- # Html special entities
- if(tag == "&"){
- if(match(t2, /^#?[A-Za-z0-9]+;/)){
- tag = tag substr(t2, RSTART, RLENGTH);
- t2 = substr(t2, RLENGTH + 1);
- return t1 tag nextil(t2);
- }
- return t1 "&" nextil(t2);
- }
- # Images
- if(tag == "!["){
- if(!match(t2, /(\[.*\])|(\(.*\))/))
- return t1 tag nextil(t2);
- match(t2, /^[^\]]*/);
- alt = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- if(match(t2, /^\(/)){
- # Inline
- sub(/^\(/, "", t2);
- match(t2, /^[^\)]+/);
- url = eschtml(substr(t2, 1, RLENGTH));
- t2 = substr(t2, RLENGTH + 2);
- title = "";
- if(match(url, /[ ]+".*"[ ]*$/)) {
- title = substr(url, RSTART, RLENGTH);
- url = substr(url, 1, RSTART - 1);
- match(title, /".*"/);
- title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\"";
- }
- if(match(url, /^<.*>$/))
- url = substr(url, 2, RLENGTH - 2);
- return t1 "<img src=\"" url "\" alt=\"" alt "\"" title " />" nextil(t2);
- }
- else{
- # Referenced
- sub(/^ ?\[/, "", t2);
- id = alt;
- if(match(t2, /^[^\]]+/))
- id = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- if(ref[id])
- r = ref[id];
- else{
- r = "<<" id;
- nr++;
- }
- return t1 "<img src=\"" r "\" alt=\"" alt "\" />" nextil(t2);
- }
- }
- # Citations
- #
- # [#id] or [locator][#id]. Numbered by first use; a repeat reuses the
- # number, matching multimarkdown.
- if(tag == "[#"){
- match(t2, /^[^\]]*/);
- cid = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- if(!(cid in cite_num)){
- cite_num[cid] = ++cite_n;
- cite_order[cite_n] = cid;
- }
- cnum = cite_num[cid];
- # A locator set by the link branch for the [locator][#id] form.
- cloc = cite_locator;
- cite_locator = "";
- clabel = (cloc != "") ? "(" cloc ", " cnum ")" : "(" cnum ")";
- return t1 "<a href=\"#cn:" cnum "\" id=\"cnref:" cnum "\" data-citation=\"" cid \
- "\" title=\"see citation\" class=\"citation\">" clabel "</a>" nextil(t2);
- }
-
- # Footnotes
- if(tag == "[^"){
- match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/);
- linktext = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- return t1 "<sup class=\"fnref\" data-fnref=\"" linktext "\"><a href=\"#fn-" linktext "\" id=\"fnref-" linktext "\">" linktext "</a></sup>" nextil(t2);
- }
- # Links
- if(tag == "["){
- # A shortcut reference is a bare [text] with no following [] or ().
- # It is only a link if a definition matching the text exists, so the
- # early return here must not fire for one -- it is decided below.
- if(!match(t2, /(\[.*\])|(\(.*\))/)){
- if(!match(t2, /^[^\]]*\]/))
- return t1 tag nextil(t2);
- shortcut_candidate = 1;
- }
- else
- shortcut_candidate = 0;
- match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/);
- linktext = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- if(match(t2, /^\(/)){
- # Inline
- match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/);
- url = substr(t2, 2, RLENGTH - 1);
- pt2 = substr(t2, RLENGTH + 2);
- title = "";
- if(match(url, /[ ]+".*"[ ]*$/)) {
- title = substr(url, RSTART, RLENGTH);
- url = substr(url, 1, RSTART - 1);
- match(title, /".*"/);
- title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\"";
- }
- if(match(url, /^<.*>$/))
- url = substr(url, 2, RLENGTH - 2);
- url = eschtml(url);
- return t1 "<a href=\"" url "\"" title ">" nextil(linktext) "</a>" nextil(pt2);
- }
- else{
- # Referenced: [text][id], [text][] or the shortcut [text].
- id = linktext;
- shortcut = 1;
- if(match(t2, /^ ?\[#/)){
- # [locator][#id] is a citation, not a reference link. The
- # locator is handed to the citation branch directly; nextil
- # has already split t1 away, so it cannot be recovered there.
- sub(/^ ?\[/, "", t2);
- cite_locator = linktext;
- return t1 nextil("[" t2);
- }
- if(match(t2, /^ ?\[/)){
- shortcut = 0;
- sub(/^ ?\[/, "", t2);
- if(match(t2, /^[^\]]*/)){
- # An empty [] means "reuse the link text as the label".
- if(RLENGTH > 0)
- id = substr(t2, 1, RLENGTH);
- t2 = substr(t2, RLENGTH + 2);
- }
- }
- id = tolower(id);
- if(ref[id])
- r = ref[id];
- else{
- # The definition may appear later in the document, so the
- # link is emitted with a placeholder that subref() fills in
- # when the definition is read. A shortcut [text] that is
- # still unresolved at EOF is restored to literal text by
- # the END rule.
- r = "<<" id;
- nr++;
- if(shortcut)
- shortcut_ref[id] = linktext;
- }
- pt2 = t2;
- return t1 "<a href=\"" r "\">" nextil(linktext) "</a>" nextil(pt2);
- }
- }
- # Strikethrough
- #
- # Only the doubled form, which is what GFM defines and what authors
- # write. A single ~ stays literal so that "~/path" and "approx ~5" are
- # not silently turned into markup. An unpaired "~~" is left alone rather
- # than opening a <del> that would run to the end of the document.
- if(tag == "~"){
- if(sub(/^~/, "", t2)){
- if(match(t2, /~~/)){
- strike = substr(t2, 1, RSTART - 1);
- # t1, tag and t2 are globals shared with every nextil frame,
- # so parsing the struck text clobbers the tail still waiting
- # here -- it came back with whatever the inner call left
- # behind, dropping the rest of the line. The link handler
- # above keeps its own copy for the same reason.
- strike_t1 = t1;
- strike_rest = substr(t2, RSTART + 2);
- strike_done = nextil(strike);
- return strike_t1 "<del>" strike_done "</del>" nextil(strike_rest);
- }
- return t1 "~~" nextil(t2);
- }
- return t1 "~" nextil(t2);
- }
- # Emphasis
- if(match(tag, /[*_]/)){
- # An underscore between two word characters is literal, not an
- # emphasis marker -- otherwise identifiers like node_modules and
- # snake_case_name get torn apart, and an unmatched one leaves a
- # stray <em> open for the rest of the document. CommonMark makes
- # the same exception, and only for _: intraword * still emphasises.
- if(tag == "_" && match(t1, /[A-Za-z0-9]$/) && match(t2, /^[A-Za-z0-9]/))
- return t1 tag nextil(t2);
- # An underscore that is the whole content of an HTML element is
- # literal: <span class="us">_</span> is a way of writing a visible
- # underscore, not emphasis. Without this it opened an <em> that
- # swallowed the markup after it, and two such underscores paired with
- # each other across the tags between them, emitting mis-nested
- # "<b><em></b> ... <b></em></b>". CommonMark reaches the same result
- # by its flanking rules: a delimiter preceded by punctuation and
- # followed by punctuation is neither opening nor closing.
- if(tag == "_" && t1 == "" && match(t2, /^</) && stag[ns] != "_" && stag[ns] != "__")
- return t1 tag nextil(t2);
- ntag = tag;
- # `tag` is a literal * or _, so it must be escaped before being
- # interpolated into a regex: "^" "*" is not a valid regex.
- etag = "^" re_quote(tag);
- if(sub(etag, "", t2)){
- if(stag[ns] == tag && match(t2, etag))
- t2 = tag t2;
- else
- ntag = tag tag
- }
- n = length(ntag);
- tag = (n == 2) ? "strong" : "em";
- if(match(t1, / $/) && match(t2, /^ /))
- return t1 tag nextil(t2);
- if(stag[ns] == ntag){
- tag = "/" tag;
- ns--;
- }
- else
- stag[++ns] = ntag;
- tag = "<" tag ">";
- return t1 tag nextil(t2);
- }
-}
-
-function inline(t) {
- ilcode = 0;
- ilcode2 = 0;
- ns = 0;
-
- return nextil(t);
-}
-
-# heading_id <text>
-# The slug a heading is addressable by: lowercased, runs of anything that is
-# not a letter or digit collapsed to a single dash, ends trimmed.
-#
-# A document long enough to link within needs its headings to be targets. Any
-# "#section" link -- from its own table of contents, from a sibling page, from
-# somewhere else entirely -- resolves to nothing without them.
-#
-# Inline markup is stripped first: a heading may be emphasised or hold code,
-# and "<code>Vision</code>" must still slug to "vision".
-#
-# Collisions are left alone. Two headings with the same words produce the same
-# id and the first wins, which is what a reader following a link expects and
-# what every other implementation does; numbering them would make the ids
-# depend on how many times a word appears earlier in the document.
-function heading_id(t, s) {
- s = t
- gsub(/<[^>]*>/, "", s)
- s = tolower(s)
- gsub(/&[a-z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/, "", s)
- gsub(/[^a-z0-9]+/, "-", s)
- sub(/^-+/, "", s)
- sub(/-+$/, "", s)
- return s
-}
-
-# para_id <html>
-# The anchor a paragraph is addressable by: "p-" and a hash of the paragraph's
-# own text.
-#
-# Derived from the content rather than the position, so a permalink survives
-# edits elsewhere on the page -- insert three paragraphs above this one, or
-# reorder the sections around it, and the link still resolves. It breaks only
-# when this paragraph's own wording changes, which is the honest failure: the
-# text someone linked to is not there any more. A positional p-7 would keep
-# resolving after an insertion and quietly point at the wrong paragraph, which
-# is worse than breaking.
-#
-# Inline markup is stripped first so that editing a link's target, or wrapping
-# a word in emphasis, does not move the anchor.
-function para_id(t, s) {
- s = t
- gsub(/<[^>]*>/, "", s)
- gsub(/&[a-z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/, "", s)
- gsub(/[ \t]+/, " ", s)
- sub(/^ /, "", s); sub(/ $/, "", s)
- if (s == "") return ""
- return "p-" fnv1a(s)
-}
-
-# fnv1a <string>
-# FNV-1a, 32 bits, in awk rather than by calling out to a hash program: a page
-# has hundreds of paragraphs and a sub-process each would be the slowest part
-# of the build. Checked against every paragraph in the site at the time it was
-# written -- no collisions between different texts.
-function fnv1a(s, h, i, c, n) {
- h = 2166136261
- n = length(s)
- for (i = 1; i <= n; i++) {
- c = index(HASHCHARS, substr(s, i, 1))
- if (c == 0) c = 32
- h = xor32(h, c)
- h = (h * 16777619) % 4294967296
- }
- return sprintf("%08x", h)
-}
-
-function xor32(a, b, r, i, x, y) {
- r = 0; i = 1
- while (a > 0 || b > 0) {
- x = a % 2; y = b % 2
- if (x != y) r += i
- a = int(a / 2); b = int(b / 2); i *= 2
- }
- return r
-}
-
-function printp(tag, id) {
- if(!match(text, /^[ ]*$/)){
- text = inline(text);
- if(tag != ""){
- # Headings carry an id; nothing else does.
- if(tag ~ /^h[1-6]$/){
- id = heading_id(text)
- if(id != "")
- oprint("<" tag " id=\"" id "\">" text "</" tag ">");
- else
- oprint("<" tag ">" text "</" tag ">");
- }
- else if(tag == "p"){
- # Two paragraphs with the same words hash the same; the second
- # and later take a suffix so every id on the page is unique.
- id = para_id(text)
- if(id != ""){
- PARA_SEEN[id]++
- if(PARA_SEEN[id] > 1)
- id = id "-" PARA_SEEN[id]
- oprint("<p id=\"" id "\">" text "</p>");
- }
- else
- oprint("<p>" text "</p>");
- }
- else
- oprint("<" tag ">" text "</" tag ">");
- }
- else
- oprint(text);
- }
- text = "";
-}
-
-BEGIN {
- # Character table for fnv1a: index() gives each byte a value, so the hash
- # needs no ord() -- which one-true-awk does not have.
- HASHCHARS = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
- blank = 0;
- code = 0;
- hr = 0;
- html = 0;
- nl = 0;
- nr = 0;
- otext = "";
- text = "";
- par = "p";
-}
-
-# References
-# Reference link definitions: [id]: url "optional title"
-# Standard markdown. The [^!id] form below predates this and is kept so any
-# document using it still resolves. Labels are case-insensitive, so they are
-# normalised to lowercase on both definition and use.
-!code && !fence && /^ *\[[^\^#\]][^\]]*\]:[ ]+/ {
- sub(/^ *\[/, "");
- match($0, /\]/);
- id = tolower(substr($0, 1, RSTART - 1));
- sub(/^[^\]]*\]:[ ]+/, "");
- title = "";
- if(match($0, /".*"$/))
- title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2);
- sub(/[ ]+".*"$/, "");
- url = eschtml($0);
- ref[id] = url title;
-
- subref(id);
- next;
-}
-
-!code && /^ *\[\^![^\]]*\]:[ ]+/ {
- sub(/^ *\[\^!/, "");
- match($0, /\]/);
- id = substr($0, 1, RSTART - 1);
- sub(id "\\]:[ ]+", "");
- id = tolower(id);
- title = "";
- if(match($0, /".*"$/))
- title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2);
- sub(/[ ]+".*"$/, "");
- url = eschtml($0);
- ref[id] = url title;
-
- subref(id);
- next;
-}
-
-!code && !fence && /^ *\[#[^\]]*\]:[ ]+/ {
- sub(/^ *\[#/, "");
- match($0, /\]/);
- id = substr($0, 1, RSTART - 1);
- sub(/^[^\]]*\]:[ ]+/, "");
- cite_body[id] = $0;
- next;
-}
-
-!code && /^ *\[\^[^\]]*\]:[ ]+/ {
- sub(/^ *\[\^/, "");
- match($0, /\]/);
- id = substr($0, 1, RSTART - 1);
- sub(id "\\]:[ ]+", "");
- sub(/[ ]+".*"$/, "");
- # Stored raw, like a citation's body: inline() escapes what needs it and
- # resolves what does not. Pre-escaping turned every "<" into "<" before
- # the parser could see it, so an autolink -- <https://example.com> -- came
- # out as literal text in a footnote while working in prose and in
- # citations. The variable was called "url" because this rule began life
- # handling link-reference definitions, where a bare URL was all it held.
- fnref[id] = $0;
-
- subref(id);
- next;
-}
-
-# html
-!html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
-isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ {
- if(code)
- oprint(code_held "</code></pre>"); code_held = "";
- for(; !text && block[nl] == "blockquote"; nl--)
- oprint("</blockquote>");
- match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
- isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/);
- htag = substr($0, 2, RLENGTH - 1);
- if(!match($0, "(<\\/" htag ">)|((^<hr ?\\/?)|(--)>$)"))
- html = 1;
- if(html && match($0, /^<hr/))
- hr = 1;
- oprint($0);
- next;
-}
-
-html && (/(^<\/(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
-isindex|menu|noframes|noscript|ol|p|pre|table|ul).*)|(--)>$/ ||
-(hr && />$/)) {
- html = 0;
- hr = 0;
- oprint($0);
- next;
-}
-
-html {
- oprint($0);
- next;
-}
-
-# MultiMarkdown metadata block
-#
-# Key: value pairs at the very top of the file, ending at the first blank
-# line. The pairs are preserved verbatim rather than stripped -- the author
-# wrote them deliberately -- but each is wrapped so a theme can style or hide
-# it, and so jenny can read the values back. Whether they are visible is a
-# readability decision for the layout, not the parser's to make.
-#
-# The entire leading run must look like metadata. If any line before the first
-# blank is not Key: value, the whole block is ordinary prose -- otherwise a
-# post opening "Note: something" would silently become metadata.
-#
-# Lines are buffered rather than consumed with getline, so the main rules see
-# them unchanged when the block turns out to be prose.
-meta_state == 0 {
- if(NR == 1 && $0 ~ /^[A-Za-z][A-Za-z0-9 _-]*:[ \t]*./){
- meta_state = 1;
- meta_n = 0;
- meta_buf[++meta_n] = $0;
- next;
- }
- meta_state = 2;
-}
-
-meta_state == 1 {
- if($0 ~ /^[ \t]*$/){
- # End of the block, and every line matched: emit them as metadata.
- for(mi = 1; mi <= meta_n; mi++){
- mline = meta_buf[mi];
- match(mline, /:/);
- mkey = substr(mline, 1, RSTART - 1);
- mval = substr(mline, RSTART + 1);
- sub(/^[ \t]+/, "", mval);
- sub(/[ \t]+$/, "", mval);
- oprint("<p class=\"meta\" data-key=\"" escattr(mkey) \
- "\" data-value=\"" escattr(mval) "\">" eschtml(mline) "</p>");
- }
- meta_state = 2;
- next;
- }
- if($0 ~ /^[A-Za-z][A-Za-z0-9 _-]*:[ \t]*./){
- meta_buf[++meta_n] = $0;
- next;
- }
- # Not a metadata block after all. The buffered lines are ordinary prose,
- # and awk cannot re-run its own rules over them, so they are pushed back
- # into the paragraph accumulator that the fall-through rule uses. That is
- # where a run of plain lines would have ended up anyway.
- meta_state = 2;
- for(mi = 1; mi <= meta_n; mi++)
- text = (text ? text " " : "") eschtml(meta_buf[mi]);
- meta_n = 0;
-}
-
-# Tables (MultiMarkdown)
-#
-# A row is only a table row if the line after it is a separator: |---|:-:|.
-# That means one line of lookahead, which is done by holding the candidate
-# header until the next line decides. Alignment comes from the colons in the
-# separator and is emitted as an inline style, matching multimarkdown's own
-# output.
-
-function table_cells(line, n, i, out) {
- # Split on unescaped pipes, dropping the optional edge pipes.
- sub(/^[ \t]*\|/, "", line);
- sub(/\|[ \t]*$/, "", line);
- return line;
-}
-
-function table_align(spec, a) {
- a = spec;
- gsub(/[ \t]/, "", a);
- if(a ~ /^:.*:$/) return "center";
- if(a ~ /:$/) return "right";
- if(a ~ /^:/) return "left";
- return "";
-}
-
-function table_row(line, celltag, n, i, cells, out, st) {
- n = split(table_cells(line), cells, "|");
- out = "<tr>";
- for(i = 1; i <= n; i++){
- sub(/^[ \t]+/, "", cells[i]);
- sub(/[ \t]+$/, "", cells[i]);
- st = "";
- if(tbl_align[i] != "")
- st = " style=\"text-align:" tbl_align[i] ";\"";
- out = out "\n\t<" celltag st ">" inline(cells[i]) "</" celltag ">";
- }
- return out "\n</tr>";
-}
-
-# A held header row, resolved by whatever follows it.
-tbl_pending != "" {
- if($0 ~ /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/){
- # It was a table after all.
- n_align = split(table_cells($0), align_spec, "|");
- for(ai = 1; ai <= n_align; ai++)
- tbl_align[ai] = table_align(align_spec[ai]);
- if(text)
- printp(par ? par : "p");
- oprint("<div class=\"table-scroll\">");
- oprint("<table>");
- oprint("<thead>");
- oprint(table_row(tbl_pending, "th"));
- oprint("</thead>");
- oprint("<tbody>");
- tbl = 1;
- tbl_pending = "";
- next;
- }
- # Not a table: the held line is ordinary text.
- text = (text ? text " " : "") tbl_pending;
- tbl_pending = "";
-}
-
-tbl {
- if($0 ~ /^[ \t]*$/ || $0 !~ /\|/){
- oprint("</tbody>");
- oprint("</table>");
- oprint("</div>");
- tbl = 0;
- for(ci in tbl_align) delete tbl_align[ci];
- if($0 ~ /^[ \t]*$/)
- next;
- }
- else{
- oprint(table_row($0, "td"));
- next;
- }
-}
-
-!fence && !code && !tbl && /\|/ && !/^[ \t]*$/ && (!text || text == "") {
- # Candidate header row: hold it and let the next line decide. Only a line
- # whose pipes separate non-empty cells qualifies, so ordinary prose that
- # happens to contain a pipe is not swallowed.
- tbl_probe = $0;
- sub(/^[ \t]*\|/, "", tbl_probe);
- sub(/\|[ \t]*$/, "", tbl_probe);
- if(index(tbl_probe, "|") > 0 || $0 ~ /^[ \t]*\|.*\|[ \t]*$/){
- tbl_pending = $0;
- next;
- }
-}
-
-# Fenced code blocks (MultiMarkdown / CommonMark)
-#
-# Must run before every other line rule: content inside a fence is literal,
-# so it has to bypass inline parsing, smart quotes and list detection alike.
-# The closing fence has to be at least as long as the opener and use the same
-# character, so a fence can contain a shorter run of backticks.
-fence {
- if($0 ~ fence_re && length($0) >= 0){
- # Candidate closer: same char, at least as long, nothing after it.
- if(match($0, "^[ \t]*" fence_char "{" fence_len ",}[ \t]*$")){
- if(fence_first)
- # An empty fence: nothing was ever buffered.
- oprint(fence_open fence_close);
- else
- oprint(fence_held fence_close);
- fence = 0;
- fence_held = "";
- next;
- }
- }
- # Hold each line until the next one arrives, so the closing tag can be
- # joined to the last line rather than sitting on its own.
- if(fence_first){
- fence_held = fence_open (fence_raw ? $0 : escfence($0));
- fence_first = 0;
- }
- else{
- oprint(fence_held);
- fence_held = (fence_raw ? $0 : escfence($0));
- }
- next;
-}
-
-/^[ \t]*(```|~~~)/ {
- # Opening fence. Everything after the run is an info string; its first
- # word is the language.
- line = $0;
- sub(/^[ \t]*/, "", line);
- fence_char = substr(line, 1, 1);
- fence_len = 0;
- while(substr(line, fence_len + 1, 1) == fence_char)
- fence_len++;
- info = substr(line, fence_len + 1);
- sub(/^[ \t]+/, "", info);
- sub(/[ \t].*$/, "", info);
-
- # Close anything currently open so the block is not nested inside a <p>.
- if(text)
- printp(par ? par : "p");
- if(code){
- oprint(code_held "</code></pre>"); code_held = "";
- code = 0;
- }
-
- fence = 1;
- fence_first = 1;
- fence_re = "^[ \t]*" fence_char;
- # A diagram language is held for a client-side renderer rather than shown
- # as source. The text goes in verbatim: mermaid reads the element's text
- # content and parses it itself, so escaping "-->" to "-->" would break
- # every arrow in the diagram. That makes this the one place a fence's
- # content is not escaped, which is safe only because the set of languages
- # treated this way is fixed here -- an author cannot introduce a new one
- # from a document.
- #
- # The language is kept as a class so a theme decides what to do with it.
- # jenny renders no diagrams itself and loads nothing; a theme that wants
- # them supplies the renderer.
- fence_raw = 0;
- fence_close = "</code></pre>";
- lang = info;
- sub(/[ \t].*$/, "", lang);
- if(lang == "mermaid" || lang == "mermaid-legend"){
- fence_raw = 1;
- fence_close = "</div>";
- fence_open = "<div class=\"diagram diagram-" eschtml(lang) "\">";
- }
- else if(info != "")
- fence_open = "<pre><code class=\"language-" eschtml(info) "\">";
- else
- fence_open = "<pre><code>";
- next;
-}
-
-# List and quote blocks
-
-# Remove indentation
-#
-# One nesting level is peeled per step. A tab or four spaces was the only unit
-# understood here, but a child is conventionally indented to its parent's
-# content column, which for "- " is two spaces -- so two-space nesting, the
-# common form, was invisible: the item regex below tolerates up to three
-# leading spaces as noise, so a two-space child matched as a sibling and the
-# whole list came out flat.
-#
-# Two spaces is the step, not an alternative to four. Written as an
-# alternation with four spaces first, a four-space indent matched the wide
-# branch and peeled a single level, so three levels of four-space nesting
-# collapsed into two. A tab remains one whole level.
-{
- for(nnl = 0; nnl < nl; nnl++)
- if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \
- (block[nnl + 1] == "blockquote" && !sub(/^> ?/, "")))
- break;
-}
-nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; }
-# Quote blocks
-{
- while(sub(/^> /, ""))
- nblock[++nnl] = "blockquote";
-}
-# Horizontal rules
-{ hr = 0; }
-(blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ {
- if(code){
- oprint(code_held "</code></pre>"); code_held = "";
- code = 0;
- }
- blank = 0;
- nnl = 0;
- hr = 1;
-}
-# List items
-block[nl] ~ /[ou]l/ && /^$/ {
- blank = 1;
- next;
-}
-{ newli = 0; }
-!hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ {
- sub(/^ ? ? ?[*+-]( +| )/, "");
- nnl++;
- nblock[nnl] = "ul";
- newli = 1;
-}
-(nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ {
- sub(/^ ? ? ?([0-9]+\.)+( +| )/, "");
- nnl++;
- nblock[nnl] = "ol";
- newli = 1;
-}
-newli {
- if(blank && nnl == nl && !par)
- par = "p";
- blank = 0;
- printp(par);
- if(nnl == nl && block[nl] == nblock[nl])
- oprint("</li><li>");
-}
-blank && ! /^$/ {
- if(match(block[nnl], /[ou]l/) && !par)
- par = "p";
- printp(par);
- par = "p";
- blank = 0;
-}
-
-# Close old blocks and open new ones
-nnl != nl || nblock[nl] != block[nl] {
- if(code){
- oprint(code_held "</code></pre>"); code_held = "";
- code = 0;
- }
- printp(par);
- b = (nnl > nl) ? nblock[nnl] : block[nnl];
- par = (match(b, /[ou]l/)) ? "" : "p";
-}
-nnl < nl || (nnl == nl && nblock[nl] != block[nl]) {
- # The second clause compares against nblock, the block kind this line
- # wants, not pblock -- which is assigned nowhere and so was always empty,
- # making the test true for any open list. Once the walk reached nnl it
- # therefore kept going and closed the outer <ul> along with the nested one,
- # leaving a dedented sibling no list to rejoin. Harmless while nesting did
- # not work at all; load-bearing now that it does.
- for(; nl > nnl || (nl > 0 && nnl == nl && nblock[nl] != block[nl]); nl--){
- if(match(block[nl], /[ou]l/))
- oprint("</li>");
- oprint("</" block[nl] ">");
- }
- # A sibling after a nested list needs a fresh <li> in the list we just came
- # back to. The "</li><li>" rule earlier only fires when the item is already
- # at its own depth (nnl == nl), which is false on a dedent.
- if(newli && nnl > 0 && nnl == nl && match(block[nl], /[ou]l/) && block[nl] == nblock[nl]){
- oprint("</li><li>");
- newli = 0;
- }
-}
-nnl > nl {
- for(; nl < nnl; nl++){
- block[nl + 1] = nblock[nl + 1];
- oprint("<" block[nl + 1] ">");
- if(match(block[nl + 1], /[ou]l/))
- oprint("<li>");
- }
-}
-hr {
- oprint("<hr>");
- next;
-}
-
-# Code blocks
-code && /^$/ {
- if(blanK)
- oprint("");
- blank = 1;
- next;
-}
-!text && sub(/^( | )/, "") {
- if(blanK)
- oprint("");
- blank = 0;
- $0 = eschtml($0);
- if(!code){
- code_held = "<pre><code>" $0;
- code = 1;
- }
- else{
- oprint(code_held);
- code_held = $0;
- }
- next;
-}
-code {
- oprint(code_held "</code></pre>");
- code_held = "";
- code = 0;
-}
-
-# Setex-style Headers
-text && /^=+$/ {printp("h1"); next;}
-text && /^-+$/ {printp("h2"); next;}
-
-# Atx-Style headers
-/^#+/ && (!newli || par=="p" || /^##/) {
- for(n = 0; n < 6 && sub(/^# */, ""); n++)
- sub(/#$/, "");
- par = "h" n;
-}
-
-# Paragraph
-/^$/ {
- printp(par);
- par = "p";
- next;
-}
-
-# Add text
-{ text = (text ? text " " : "") $0; }
-
-function alen(a, ix, k) {
- k = 0
- for(ix in a) k++
- return k
-}
-
-END {
- # A candidate header row with no separator after it was ordinary text.
- if(tbl_pending != ""){
- text = (text ? text " " : "") tbl_pending;
- tbl_pending = "";
- }
- if(tbl){
- oprint("</tbody>");
- oprint("</table>");
- oprint("</div>");
- tbl = 0;
- }
- if(code){
- oprint(code_held "</code></pre>"); code_held = "";
- code = 0;
- }
- printp(par);
- for(; nl > 0; nl--){
- if(match(block[nl], /[ou]l/))
- oprint("</li>");
- oprint("</" block[nl] ">");
- }
- # A reference whose definition never appeared is not a link at all.
- # Restore the literal text the author wrote rather than emitting an
- # anchor with an empty href.
- for(sid in shortcut_ref){
- lt = shortcut_ref[sid];
- gsub("<a href=\"<<" sid "\">" lt "</a>", "[" lt "]", otext);
- }
- while(match(otext, /<a href="<<[^"]*">[^<]*<\/a>/)){
- frag = substr(otext, RSTART, RLENGTH);
- lt = frag;
- sub(/^<a href="<<[^"]*">/, "", lt);
- sub(/<\/a>$/, "", lt);
- rid = frag;
- sub(/^<a href="<</, "", rid);
- sub(/">.*$/, "", rid);
- otext = substr(otext, 1, RSTART - 1) "[" lt "][" rid "]" \
- substr(otext, RSTART + RLENGTH);
- }
- gsub(/<<[^"]*/, "", otext);
- print(otext);
-
- # Print citations, in order of first use.
- if(cite_n > 0) {
- print "<div class=\"citations\">";
- print "<hr />";
- print "<ol>";
- for(ci = 1; ci <= cite_n; ci++){
- cid = cite_order[ci];
- print "<li id=\"cn:" ci "\"><p>" inline(cite_body[cid]) \
- " <a href=\"#cnref:" ci "\" title=\"return to body\" class=\"reversecitation\"> ↩︎</a></p></li>";
- }
- print "</ol>";
- print "</div>";
- }
-
- # Print footnotes
- if(alen(fnref)>0) {
- print "<ul class=\"fn-list\">";
- for (i in fnref) print "<li id=\"fn-" i "\" class=\"fn-item\"><span class=\"fn-handle\">" i ": </span><span class=\"fn-text\">" inline(fnref[i]) " <a href=\"#fnref-" i "\" class=\"fn-backref\">↩︎</a></span></li>";
- print "</ul>";
- }
-}
diff --git a/test/regressions.sh b/test/regressions.sh
index 0c76397..8db745f 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -767,7 +767,7 @@ suite "nested lists"
# two spaces for "- ". Two-space nesting, the common form, came out flat.
md_render() {
- printf '%s\n' "$1" | "$AWK" -f "$JENNY_SHARE_DIR/lib/md2html.awk"
+ printf '%s\n' "$1" | "$AWK" -f "$JENNY_SHARE_DIR/lib/markdown.awk/markdown.awk"
}
it "a two-space child nests inside its parent"
diff --git a/test/units.sh b/test/units.sh
index f6f41d1..d73a960 100755
--- a/test/units.sh
+++ b/test/units.sh
@@ -306,7 +306,13 @@ URL_STYLE=""
suite "markdown: intraword underscores"
-md() { printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/share/jenny/lib/md2html.awk"; }
+# The deep-link id is stripped so these assertions can state the markup they
+# are about. Each is checking how a span of text is parsed, not that a
+# paragraph carries an anchor -- that has tests of its own, and pinning a
+# content hash in every one of these would make them unreadable and would
+# break them all again on any wording change.
+md() { printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/share/jenny/lib/markdown.awk/markdown.awk" \
+ | "$SED" 's/<p id="p-[0-9a-f]*\(-[0-9]*\)\{0,1\}">/<p>/g'; }
it "an underscore inside a word is literal"
assert_eq "$(md 'node_modules here')" "<p>node_modules here</p>"