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 < > &
26// " 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 // <
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 // >
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 // &
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 // "
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 " 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}