code wiki / (root) / nx_md_render.nx

nx_md_render.nx source

↩ module page · 442 lines · 19011 B

1// nx_md_render.nx -- sovereign Markdown -> HTML renderer. 2// 3// Single-pass. Scan input bytes line-by-line; emit HTML bytes. 4// CommonMark subset suitable for andelinwest.com and similar 5// human-written content: 6// 7// # Heading -> <h1>Heading</h1> 8// ## Heading 2 -> <h2>Heading 2</h2> ... up to ###### 9// normal paragraph -> <p>normal paragraph</p> 10// - list item -> <ul><li>list item</li>...</ul> 11// *italic* -> <em>italic</em> 12// **bold** -> <strong>bold</strong> 13// `code` -> <code>code</code> 14// [text](url) -> <a href="url">text</a> 15// --- -> <hr /> 16// blank line -> closes the current paragraph or list 17// ``` -> toggle pre-formatted code block 18// 19// Not yet supported (named improvement, queued L10b): 20// tables, blockquotes, ordered lists, nested lists, images, footnotes, 21// reference links, HTML pass-through. These mostly compose by adding 22// one more transition state to the scanner; the substrate stays the 23// same shape. 24// 25// Escaping: <, >, &, " in input bytes are emitted as &lt; &gt; &amp; 26// &quot; in output (skipping HTML pass-through for safety). 27// 28// genealogy_id: commonmark_0_30 + cmark_2014 + markdown_perl_2004 29// lineage_id: nishi_markdown_subset_q10 30 31// nx_safety_envelope: 32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 33// sil_target: SIL1 34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 35// verdict: NOT_YET_EVALUATED 36 37// Unified syscalls per-ISA via @ifdef TARGET_X86_64 (commit 164209c9) 38// avoids symbol collision when this module is composed with others 39// that import nx_syscalls.nx. 40import "nx_syscalls.nx" 41 42// Emit a single byte to out[off]; return new off. 43func _emit1(out: *u8, off: i64, b: i64) -> i64 { 44 out[off] = b 45 return off + 1 46} 47 48// Emit n bytes from src. 49func _emitn(out: *u8, off: i64, src: *u8, n: i64) -> i64 { 50 var i: i64 = 0 51 while i < n { out[off + i] = src[i]; i = i + 1 } 52 return off + n 53} 54 55// Emit a literal short string by writing each byte. Inlined by caller. 56 57// HTML-escape a single byte; returns new off after emitting 1..5 bytes. 58func _emit_esc1(out: *u8, off: i64, b: i64) -> i64 { 59 if b == 60 { // '<' 60 out[off]=38; out[off+1]=108; out[off+2]=116; out[off+3]=59 // &lt; 61 return off + 4 62 } 63 if b == 62 { // '>' 64 out[off]=38; out[off+1]=103; out[off+2]=116; out[off+3]=59 // &gt; 65 return off + 4 66 } 67 if b == 38 { // '&' 68 out[off]=38; out[off+1]=97; out[off+2]=109; out[off+3]=112; out[off+4]=59 // &amp; 69 return off + 5 70 } 71 if b == 34 { // '"' 72 out[off]=38; out[off+1]=113; out[off+2]=117; out[off+3]=111; out[off+4]=116; out[off+5]=59 // &quot; 73 return off + 6 74 } 75 out[off] = b 76 return off + 1 77} 78 79// Emit an inline run: walk src[0..n), handling **bold**, *italic*, 80// `code`, [text](url). All other bytes get HTML-escaped 1:1. 81func _emit_inline(out: *u8, off_in: i64, src: *u8, n: i64) -> i64 { 82 var off: i64 = off_in 83 var i: i64 = 0 84 while i < n { 85 let c: i64 = src[i] 86 // **bold** 87 if c == 42 { 88 if i + 1 < n { 89 if src[i+1] == 42 { 90 // Find closing ** 91 var j: i64 = i + 2 92 var found: i64 = -1 93 while j + 1 < n { 94 if src[j] == 42 { 95 if src[j+1] == 42 { found = j; j = n } 96 else { j = j + 1 } 97 } else { j = j + 1 } 98 } 99 if found >= 0 { 100 out[off]=60; out[off+1]=115; out[off+2]=116; out[off+3]=114; out[off+4]=111; out[off+5]=110; out[off+6]=103; out[off+7]=62 // <strong> 101 off = off + 8 102 var k: i64 = i + 2 103 while k < found { off = _emit_esc1(out, off, src[k]); k = k + 1 } 104 out[off]=60; out[off+1]=47; out[off+2]=115; out[off+3]=116; out[off+4]=114; out[off+5]=111; out[off+6]=110; out[off+7]=103; out[off+8]=62 // </strong> 105 off = off + 9 106 i = found + 2 107 // loop tail: skip the i = i + 1 below 108 var _skip: i64 = 1 109 if _skip == 1 { var _x: i64 = 0 } 110 // continue outer 111 } else { 112 // No closing; emit literal * 113 off = _emit_esc1(out, off, c) 114 i = i + 1 115 } 116 } else { 117 // *italic* 118 var j: i64 = i + 1 119 var found: i64 = -1 120 while j < n { 121 if src[j] == 42 { found = j; j = n } 122 else { j = j + 1 } 123 } 124 if found > i + 1 { 125 out[off]=60; out[off+1]=101; out[off+2]=109; out[off+3]=62 // <em> 126 off = off + 4 127 var k: i64 = i + 1 128 while k < found { off = _emit_esc1(out, off, src[k]); k = k + 1 } 129 out[off]=60; out[off+1]=47; out[off+2]=101; out[off+3]=109; out[off+4]=62 // </em> 130 off = off + 5 131 i = found + 1 132 } else { 133 off = _emit_esc1(out, off, c) 134 i = i + 1 135 } 136 } 137 } else { 138 off = _emit_esc1(out, off, c) 139 i = i + 1 140 } 141 } 142 else { 143 // `code` 144 if c == 96 { 145 var j: i64 = i + 1 146 var found: i64 = -1 147 while j < n { 148 if src[j] == 96 { found = j; j = n } 149 else { j = j + 1 } 150 } 151 if found > i { 152 out[off]=60; out[off+1]=99; out[off+2]=111; out[off+3]=100; out[off+4]=101; out[off+5]=62 // <code> 153 off = off + 6 154 var k: i64 = i + 1 155 while k < found { off = _emit_esc1(out, off, src[k]); k = k + 1 } 156 out[off]=60; out[off+1]=47; out[off+2]=99; out[off+3]=111; out[off+4]=100; out[off+5]=101; out[off+6]=62 // </code> 157 off = off + 7 158 i = found + 1 159 } else { 160 off = _emit_esc1(out, off, c) 161 i = i + 1 162 } 163 } 164 else { 165 // [text](url) 166 if c == 91 { 167 // Find ']' 168 var rb: i64 = i + 1 169 var rb_found: i64 = -1 170 while rb < n { 171 if src[rb] == 93 { rb_found = rb; rb = n } 172 else { rb = rb + 1 } 173 } 174 var ok: i64 = 0 175 var url_end: i64 = -1 176 if rb_found > i { 177 if rb_found + 1 < n { 178 if src[rb_found + 1] == 40 { 179 var p: i64 = rb_found + 2 180 while p < n { 181 if src[p] == 41 { url_end = p; p = n } 182 else { p = p + 1 } 183 } 184 if url_end > rb_found + 1 { ok = 1 } 185 } 186 } 187 } 188 if ok == 1 { 189 // <a href=" 190 out[off]=60; out[off+1]=97; out[off+2]=32; out[off+3]=104; out[off+4]=114; out[off+5]=101; out[off+6]=102; out[off+7]=61; out[off+8]=34 191 off = off + 9 192 var k: i64 = rb_found + 2 193 while k < url_end { off = _emit_esc1(out, off, src[k]); k = k + 1 } 194 out[off]=34; out[off+1]=62 // "> 195 off = off + 2 196 var t: i64 = i + 1 197 while t < rb_found { off = _emit_esc1(out, off, src[t]); t = t + 1 } 198 out[off]=60; out[off+1]=47; out[off+2]=97; out[off+3]=62 // </a> 199 off = off + 4 200 i = url_end + 1 201 } else { 202 off = _emit_esc1(out, off, c) 203 i = i + 1 204 } 205 } 206 else { 207 off = _emit_esc1(out, off, c) 208 i = i + 1 209 } 210 } 211 } 212 } 213 return off 214} 215 216// State: 0 = none, 1 = inside paragraph, 2 = inside list, 3 = inside code block 217const NX_MD_STATE_NONE: i64 = 0 218const NX_MD_STATE_PARA: i64 = 1 219const NX_MD_STATE_LIST: i64 = 2 220const NX_MD_STATE_CODE: i64 = 3 221 222// Close whatever block is open. Returns new off + new state (=NONE). 223func _close_block(out: *u8, off_in: i64, state: i64) -> i64 { 224 var off: i64 = off_in 225 if state == NX_MD_STATE_PARA { 226 out[off]=60; out[off+1]=47; out[off+2]=112; out[off+3]=62; out[off+4]=10 // </p>\n 227 off = off + 5 228 } 229 if state == NX_MD_STATE_LIST { 230 out[off]=60; out[off+1]=47; out[off+2]=117; out[off+3]=108; out[off+4]=62; out[off+5]=10 // </ul>\n 231 off = off + 6 232 } 233 if state == NX_MD_STATE_CODE { 234 out[off]=60; out[off+1]=47; out[off+2]=112; out[off+3]=114; out[off+4]=101; out[off+5]=62; out[off+6]=10 // </pre>\n 235 off = off + 7 236 } 237 return off 238} 239 240// Top-level: read markdown bytes from src, write HTML bytes into out. 241// Returns the number of HTML bytes written. Caller must ensure `out` 242// is at least 4x the size of `src` (generous: each char can expand 243// to 5-byte &quot; etc). 244func nx_md_render(src: *u8, n: i64, out: *u8) -> i64 { 245 var off: i64 = 0 246 var state: i64 = NX_MD_STATE_NONE 247 var i: i64 = 0 248 while i < n { 249 // Find end of this line (next \n or EOF). 250 var eol: i64 = i 251 while eol < n { 252 if src[eol] == 10 { eol = eol + 1; eol = n + 1 } 253 else { eol = eol + 1 } 254 } 255 var line_end: i64 = eol 256 if eol == n + 1 { line_end = n } // we walked past \n; restore 257 // Actually re-scan plainly to find the newline cleanly. 258 line_end = i 259 while line_end < n { 260 if src[line_end] == 10 { line_end = n + 1 } 261 else { line_end = line_end + 1 } 262 } 263 if line_end == n + 1 { line_end = line_end - 1 } // unused branch 264 // simpler: find \n 265 var nl: i64 = i 266 while nl < n { 267 if src[nl] == 10 { nl = n + 1 } 268 else { nl = nl + 1 } 269 } 270 var line_len: i64 = 0 271 if nl == n + 1 { 272 // Found newline at nl-1 (because we set nl=n+1 to break) 273 // Recover: scan again for \n position 274 var p: i64 = i 275 while p < n { 276 if src[p] == 10 { line_len = p - i; p = n } 277 else { p = p + 1 } 278 } 279 } 280 if nl < n + 1 { 281 // No newline -> last line 282 line_len = n - i 283 } 284 let line_start: i64 = i 285 286 // Decide line kind. 287 let first: i64 = src[line_start] 288 289 // Inside fenced code block: emit literally until closing ```. 290 if state == NX_MD_STATE_CODE { 291 if line_len >= 3 { 292 if src[line_start] == 96 { 293 if src[line_start+1] == 96 { 294 if src[line_start+2] == 96 { 295 off = _close_block(out, off, state) 296 state = NX_MD_STATE_NONE 297 i = line_start + line_len + 1 298 } else { 299 var k: i64 = 0 300 while k < line_len { off = _emit_esc1(out, off, src[line_start+k]); k = k + 1 } 301 off = _emit1(out, off, 10) 302 i = line_start + line_len + 1 303 } 304 } else { 305 var k: i64 = 0 306 while k < line_len { off = _emit_esc1(out, off, src[line_start+k]); k = k + 1 } 307 off = _emit1(out, off, 10) 308 i = line_start + line_len + 1 309 } 310 } else { 311 var k: i64 = 0 312 while k < line_len { off = _emit_esc1(out, off, src[line_start+k]); k = k + 1 } 313 off = _emit1(out, off, 10) 314 i = line_start + line_len + 1 315 } 316 } else { 317 var k: i64 = 0 318 while k < line_len { off = _emit_esc1(out, off, src[line_start+k]); k = k + 1 } 319 off = _emit1(out, off, 10) 320 i = line_start + line_len + 1 321 } 322 } 323 else { 324 // Blank line closes any open block. 325 if line_len == 0 { 326 off = _close_block(out, off, state) 327 state = NX_MD_STATE_NONE 328 i = line_start + 1 329 } 330 else { 331 // Fenced code start 332 if line_len >= 3 { 333 if first == 96 { 334 var fence: i64 = 0 335 if src[line_start+1] == 96 { 336 if src[line_start+2] == 96 { fence = 1 } 337 } 338 if fence == 1 { 339 off = _close_block(out, off, state) 340 state = NX_MD_STATE_CODE 341 out[off]=60; out[off+1]=112; out[off+2]=114; out[off+3]=101; out[off+4]=62; out[off+5]=10 // <pre>\n 342 off = off + 6 343 i = line_start + line_len + 1 344 } 345 } 346 } 347 if i == line_start { 348 // hr: --- on its own line 349 if line_len == 3 { 350 if src[line_start] == 45 { 351 if src[line_start+1] == 45 { 352 if src[line_start+2] == 45 { 353 off = _close_block(out, off, state) 354 state = NX_MD_STATE_NONE 355 out[off]=60; out[off+1]=104; out[off+2]=114; out[off+3]=32; out[off+4]=47; out[off+5]=62; out[off+6]=10 // <hr />\n 356 off = off + 7 357 i = line_start + line_len + 1 358 } 359 } 360 } 361 } 362 } 363 if i == line_start { 364 // Heading: count leading #s (1..6). Use a separate 365 // "done" flag so the count stays accurate after the 366 // loop exits at the first non-#. 367 var h: i64 = 0 368 var counting: i64 = 1 369 while counting == 1 { 370 if h >= 6 { counting = 0 } 371 else { 372 if line_start + h >= line_start + line_len { counting = 0 } 373 else { 374 if src[line_start + h] == 35 { h = h + 1 } 375 else { counting = 0 } 376 } 377 } 378 } 379 if h > 0 { 380 if h <= 6 { 381 if line_start + h < line_start + line_len { 382 if src[line_start + h] == 32 { 383 off = _close_block(out, off, state) 384 state = NX_MD_STATE_NONE 385 out[off]=60; out[off+1]=104; off=off+2 386 out[off]=48 + h; off=off+1 387 out[off]=62; off=off+1 388 // text starts at line_start + h + 1 389 let text_off: i64 = line_start + h + 1 390 let text_len: i64 = line_len - h - 1 391 off = _emit_inline(out, off, (src as i64 + text_off) as *u8, text_len) 392 out[off]=60; out[off+1]=47; out[off+2]=104; off=off+3 393 out[off]=48 + h; off=off+1 394 out[off]=62; out[off+1]=10; off=off+2 395 i = line_start + line_len + 1 396 } 397 } 398 } 399 } 400 } 401 if i == line_start { 402 // List item: "- " prefix 403 if line_len >= 2 { 404 if first == 45 { 405 if src[line_start + 1] == 32 { 406 if state != NX_MD_STATE_LIST { 407 off = _close_block(out, off, state) 408 out[off]=60; out[off+1]=117; out[off+2]=108; out[off+3]=62; out[off+4]=10 // <ul>\n 409 off = off + 5 410 state = NX_MD_STATE_LIST 411 } 412 out[off]=60; out[off+1]=108; out[off+2]=105; out[off+3]=62 // <li> 413 off = off + 4 414 off = _emit_inline(out, off, (src as i64 + line_start + 2) as *u8, line_len - 2) 415 out[off]=60; out[off+1]=47; out[off+2]=108; out[off+3]=105; out[off+4]=62; out[off+5]=10 // </li>\n 416 off = off + 6 417 i = line_start + line_len + 1 418 } 419 } 420 } 421 } 422 if i == line_start { 423 // Paragraph text. 424 if state != NX_MD_STATE_PARA { 425 off = _close_block(out, off, state) 426 out[off]=60; out[off+1]=112; out[off+2]=62 // <p> 427 off = off + 3 428 state = NX_MD_STATE_PARA 429 } else { 430 // Continued paragraph: emit a space between lines. 431 out[off] = 32; off = off + 1 432 } 433 off = _emit_inline(out, off, (src as i64 + line_start) as *u8, line_len) 434 i = line_start + line_len + 1 435 } 436 } 437 } 438 } 439 // Close any open block at EOF. 440 off = _close_block(out, off, state) 441 return off 442}