code wiki / (root) / nx_markdown_block.nx

nx_markdown_block.nx source

↩ module page · 359 lines · 12239 B

1// markdown_block.nx -- block-level Markdown -> HTML. 2// 3// CommonMark §4 + §5 practical subset. Walks input line-by-line 4// recognising block constructs, sends runs of text through 5// markdown_inline for inline formatting. 6// 7// Supported block-level constructs: 8// # Heading -> <h1>...</h1> (levels 1-6 by # count) 9// ## Subheading -> <h2>...</h2> 10// (blank line) -> closes current paragraph 11// ```fenced -> <pre><code>...</code></pre> 12// - bullet -> <ul><li>...</li></ul> 13// * bullet -> <ul><li>...</li></ul> 14// 1. ordered -> <ol><li>...</li></ol> 15// regular text -> <p>...</p> 16// 17// NOT supported (future markdown_block_ext.nx): tables, blockquotes, 18// setext headings (=== / ---), horizontal rules, link reference 19// definitions, indented code blocks. 20// 21// Composes markdown_inline + html_escape. 22// 23// Invariants: 24// B1 Fenced code block content is NOT run through inline 25// markdown -- raw text with html_escape only. 26// B2 List items have inline markdown applied to their content. 27// B3 Consecutive list items collapse into one <ul> / <ol>. 28// B4 Output is valid HTML fragment (no <html>/<head>/<body>). 29// 30// nx_safety_envelope: 31// intended_use: "Markdown block-level parser (CommonMark 32// section 4) -- docs / README / nishi-library 33// catalog ingestion" 34// sil_target: SIL1 35// asil_target: QM 36// dal_target: NONE 37// evidence: [CommonMark_0_30_canonical_basis, 38// sealed_block_kind_enum, no_FP, 39// bounded_block_parse_state_machine] 40// hazard_register: [bug-tape-XSS-via-raw-HTML-passthrough, 41// bug-tape-list-nesting-depth-bomb] 42// residual_risk: "Output HTML must be sanitized by consumer 43// (substrate does not strip raw HTML)." 44// verdict: NOT_YET_EVALUATED 45 46import "nx_syscalls.nx" 47import "nx_markdown_inline.nx" 48import "nx_html_escape.nx" 49const MB_MAGIC_2048: i64 = 2048 50 51const MB_ERR_SHORT: i64 = -1 52 53func mb_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 { 54 if off + n > cap { return MB_ERR_SHORT } 55 var i: i64 = 0 56 while i < n { 57 out[off + i] = src[i] 58 i = i + 1 59 } 60 return off + n 61} 62 63// Render src[a..b] as inline markdown into out[off..]. 64func mb_put_inline(out: *u8, cap: i64, off: i64, 65 src: *u8, a: i64, b: i64) -> i64 { 66 let w: i64 = markdown_inline(src + a, b - a, out + off, cap - off) 67 if w < 0 { return MB_ERR_SHORT } 68 return off + w 69} 70 71// Render src[a..b] as html-escaped (for code blocks). 72func mb_put_esc(out: *u8, cap: i64, off: i64, 73 src: *u8, a: i64, b: i64) -> i64 { 74 let w: i64 = html_escape(out + off, cap - off, src + a, b - a) 75 if w < 0 { return MB_ERR_SHORT } 76 return off + w 77} 78 79// Find end of line from off; returns position of LF (or n). 80func mb_eol(buf: *u8, n: i64, off: i64) -> i64 { 81 var i: i64 = off 82 while i < n { 83 if buf[i] == 0x0A { return i } 84 i = i + 1 85 } 86 return n 87} 88 89// Count leading '#' chars up to max 6; returns count (0 if none). 90func mb_heading_level(buf: *u8, off: i64, eol: i64) -> i64 { 91 var i: i64 = off 92 var c: i64 = 0 93 while i < eol { 94 if buf[i] != 0x23 { break } 95 c = c + 1 96 i = i + 1 97 if c >= 6 { break } 98 } 99 // Must be followed by a space (CommonMark §4.2). 100 if i < eol { 101 if buf[i] != 0x20 { return 0 } 102 } 103 return c 104} 105 106// Is the trimmed line a fence line "```" (at least 3)? 107func mb_is_fence(buf: *u8, off: i64, eol: i64) -> i64 { 108 if eol - off < 3 { return 0 } 109 if buf[off] != 0x60 { return 0 } 110 if buf[off + 1] != 0x60 { return 0 } 111 if buf[off + 2] != 0x60 { return 0 } 112 return 1 113} 114 115// Is this line a bullet (starts with '-' or '*' + space)? 116// Returns column of the content after the bullet, or -1 if not 117// a bullet. 118func mb_is_bullet(buf: *u8, off: i64, eol: i64) -> i64 { 119 if off + 1 >= eol { return -1 } 120 let c: i64 = buf[off] 121 if c != 0x2D { 122 if c != 0x2A { return -1 } 123 } 124 if buf[off + 1] != 0x20 { return -1 } 125 return off + 2 126} 127 128// Is this line an ordered-list item ("1." / "12." etc.)? 129// Returns content start offset or -1. 130func mb_is_ordered(buf: *u8, off: i64, eol: i64) -> i64 { 131 var i: i64 = off 132 var any: i64 = 0 133 while i < eol { 134 let b: i64 = buf[i] 135 if b < 0x30 { break } 136 if b > 0x39 { break } 137 any = 1 138 i = i + 1 139 } 140 if any == 0 { return -1 } 141 if i >= eol { return -1 } 142 if buf[i] != 0x2E { return -1 } 143 i = i + 1 144 if i >= eol { return -1 } 145 if buf[i] != 0x20 { return -1 } 146 return i + 1 147} 148 149// Is this line empty / whitespace-only? 150func mb_is_blank(buf: *u8, off: i64, eol: i64) -> i64 { 151 var i: i64 = off 152 while i < eol { 153 let b: i64 = buf[i] 154 if b != 0x20 { 155 if b != 0x09 { return 0 } 156 } 157 i = i + 1 158 } 159 return 1 160} 161 162// Main block walker. 163func markdown_block(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 164 var pos: i64 = 0 165 var out_pos: i64 = 0 166 var in_ul: i64 = 0 167 var in_ol: i64 = 0 168 169 while pos < n { 170 let eol: i64 = mb_eol(src, n, pos) 171 172 // Blank line? 173 if mb_is_blank(src, pos, eol) == 1 { 174 // Close any open list. 175 if in_ul == 1 { 176 out_pos = mb_put(out, cap, out_pos, "</ul>\n", 6) 177 if out_pos < 0 { return out_pos } 178 in_ul = 0 179 } 180 if in_ol == 1 { 181 out_pos = mb_put(out, cap, out_pos, "</ol>\n", 6) 182 if out_pos < 0 { return out_pos } 183 in_ol = 0 184 } 185 pos = eol + 1 186 continue 187 } 188 189 // Fenced code? 190 if mb_is_fence(src, pos, eol) == 1 { 191 out_pos = mb_put(out, cap, out_pos, "<pre><code>", 11) 192 if out_pos < 0 { return out_pos } 193 pos = eol + 1 194 while pos < n { 195 let inner_eol: i64 = mb_eol(src, n, pos) 196 if mb_is_fence(src, pos, inner_eol) == 1 { 197 pos = inner_eol + 1 198 break 199 } 200 out_pos = mb_put_esc(out, cap, out_pos, 201 src, pos, inner_eol) 202 if out_pos < 0 { return out_pos } 203 out_pos = mb_put(out, cap, out_pos, "\n", 1) 204 if out_pos < 0 { return out_pos } 205 pos = inner_eol + 1 206 } 207 out_pos = mb_put(out, cap, out_pos, "</code></pre>\n", 14) 208 if out_pos < 0 { return out_pos } 209 continue 210 } 211 212 // Heading? 213 let hlvl: i64 = mb_heading_level(src, pos, eol) 214 if hlvl > 0 { 215 let open: *u8 = "<h1>" 216 // Write h-tag with dynamic level. 217 if out_pos + 4 > cap { return MB_ERR_SHORT } 218 out[out_pos] = 0x3C 219 out[out_pos + 1] = 0x68 220 out[out_pos + 2] = 0x30 + hlvl 221 out[out_pos + 3] = 0x3E 222 out_pos = out_pos + 4 223 let content_start: i64 = pos + hlvl + 1 // skip # + space 224 out_pos = mb_put_inline(out, cap, out_pos, 225 src, content_start, eol) 226 if out_pos < 0 { return out_pos } 227 if out_pos + 6 > cap { return MB_ERR_SHORT } 228 out[out_pos] = 0x3C 229 out[out_pos + 1] = 0x2F 230 out[out_pos + 2] = 0x68 231 out[out_pos + 3] = 0x30 + hlvl 232 out[out_pos + 4] = 0x3E 233 out[out_pos + 5] = 0x0A 234 out_pos = out_pos + 6 235 pos = eol + 1 236 continue 237 } 238 239 // Bullet list? 240 let b_start: i64 = mb_is_bullet(src, pos, eol) 241 if b_start >= 0 { 242 if in_ul == 0 { 243 if in_ol == 1 { 244 out_pos = mb_put(out, cap, out_pos, "</ol>\n", 6) 245 if out_pos < 0 { return out_pos } 246 in_ol = 0 247 } 248 out_pos = mb_put(out, cap, out_pos, "<ul>\n", 5) 249 if out_pos < 0 { return out_pos } 250 in_ul = 1 251 } 252 out_pos = mb_put(out, cap, out_pos, "<li>", 4) 253 if out_pos < 0 { return out_pos } 254 out_pos = mb_put_inline(out, cap, out_pos, src, b_start, eol) 255 if out_pos < 0 { return out_pos } 256 out_pos = mb_put(out, cap, out_pos, "</li>\n", 6) 257 if out_pos < 0 { return out_pos } 258 pos = eol + 1 259 continue 260 } 261 262 // Ordered list? 263 let o_start: i64 = mb_is_ordered(src, pos, eol) 264 if o_start >= 0 { 265 if in_ol == 0 { 266 if in_ul == 1 { 267 out_pos = mb_put(out, cap, out_pos, "</ul>\n", 6) 268 if out_pos < 0 { return out_pos } 269 in_ul = 0 270 } 271 out_pos = mb_put(out, cap, out_pos, "<ol>\n", 5) 272 if out_pos < 0 { return out_pos } 273 in_ol = 1 274 } 275 out_pos = mb_put(out, cap, out_pos, "<li>", 4) 276 if out_pos < 0 { return out_pos } 277 out_pos = mb_put_inline(out, cap, out_pos, src, o_start, eol) 278 if out_pos < 0 { return out_pos } 279 out_pos = mb_put(out, cap, out_pos, "</li>\n", 6) 280 if out_pos < 0 { return out_pos } 281 pos = eol + 1 282 continue 283 } 284 285 // Fallback: paragraph. 286 if in_ul == 1 { 287 out_pos = mb_put(out, cap, out_pos, "</ul>\n", 6) 288 if out_pos < 0 { return out_pos } 289 in_ul = 0 290 } 291 if in_ol == 1 { 292 out_pos = mb_put(out, cap, out_pos, "</ol>\n", 6) 293 if out_pos < 0 { return out_pos } 294 in_ol = 0 295 } 296 out_pos = mb_put(out, cap, out_pos, "<p>", 3) 297 if out_pos < 0 { return out_pos } 298 out_pos = mb_put_inline(out, cap, out_pos, src, pos, eol) 299 if out_pos < 0 { return out_pos } 300 out_pos = mb_put(out, cap, out_pos, "</p>\n", 5) 301 if out_pos < 0 { return out_pos } 302 pos = eol + 1 303 } 304 305 // Close any trailing open list. 306 if in_ul == 1 { 307 out_pos = mb_put(out, cap, out_pos, "</ul>\n", 6) 308 if out_pos < 0 { return out_pos } 309 } 310 if in_ol == 1 { 311 out_pos = mb_put(out, cap, out_pos, "</ol>\n", 6) 312 if out_pos < 0 { return out_pos } 313 } 314 return out_pos 315} 316 317// Compile-only smoke. 318func main() -> i64 { 319 let out: *u8 = sys_mmap(MB_MAGIC_2048) 320 321 // Heading. 322 let n1: i64 = markdown_block("# Hello\n", 8, out, MB_MAGIC_2048) 323 if n1 <= 0 { return 1 } 324 if out[0] != 0x3C { return 2 } // '<' 325 if out[1] != 0x68 { return 3 } // 'h' 326 if out[2] != 0x31 { return 4 } // '1' 327 328 // Paragraph + bullet list. 329 let post: *u8 = "Hello world\n\n- one\n- two\n" 330 let n2: i64 = markdown_block(post, 25, out, MB_MAGIC_2048) 331 if n2 <= 0 { return 5 } 332 // Must contain <ul> and </ul> 333 var has_ul: i64 = 0 334 var i: i64 = 0 335 while i < n2 - 3 { 336 if out[i] == 0x3C { 337 if out[i + 1] == 0x75 { // 'u' 338 if out[i + 2] == 0x6C { // 'l' 339 if out[i + 3] == 0x3E { // '>' 340 has_ul = 1 341 break 342 } 343 } 344 } 345 } 346 i = i + 1 347 } 348 if has_ul != 1 { return 6 } 349 350 // Fenced code. 351 let code: *u8 = "```\nlet x = 1\n```\n" 352 let n3: i64 = markdown_block(code, 18, out, MB_MAGIC_2048) 353 if n3 <= 0 { return 7 } 354 // Starts with <pre><code> 355 if out[0] != 0x3C { return 8 } 356 if out[1] != 0x70 { return 9 } // 'p' 357 if out[2] != 0x72 { return 10 } // 'r' 358 return 0 359}