code wiki / (root) / nx_markdown_inline.nx

nx_markdown_inline.nx source

↩ module page · 235 lines · 8232 B

1// markdown_inline.nx -- render inline Markdown to HTML. 2// 3// Inline-level only -- headings / paragraphs / lists / code 4// blocks belong in a future markdown_block.nx that walks line- 5// by-line and feeds each line through THIS module. Splitting 6// inline from block keeps each module small + testable. 7// 8// Supported inline constructs (CommonMark ยง6, practical subset): 9// *em* -> <em>em</em> 10// _em_ -> <em>em</em> 11// **strong** -> <strong>strong</strong> 12// __strong__ -> <strong>strong</strong> 13// `code` -> <code>code</code> 14// [label](href) -> <a href=\"href\">label</a> 15// 16// HTML-unsafe characters in surrounding text (<, >, &, \", ') 17// pass through html_escape.nx so we don't re-introduce XSS. 18// 19// Composes html_escape.nx. 20// 21// Invariants: 22// MD1 No nested emphasis parsing today -- *outer *inner* 23// outer* renders naively (matches emphasis greedily). 24// MD2 Code spans (`...`) disable markup inside; their content 25// IS html-escaped so `<script>` renders as code. 26// MD3 Link href is NOT validated or escaped against 27// javascript: / data: schemes. For untrusted content 28// callers should pre-filter href before feeding us. 29 30// nx_safety_envelope: 31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 32// sil_target: SIL1 33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 34// verdict: NOT_YET_EVALUATED 35 36import "nx_syscalls.nx" 37import "nx_html_escape.nx" 38 39const MD_ERR_SHORT: i64 = -1 40 41func md_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 { 42 if off + n > cap { return MD_ERR_SHORT } 43 var i: i64 = 0 44 while i < n { 45 out[off + i] = src[i] 46 i = i + 1 47 } 48 return off + n 49} 50 51// Emit html_escape(src[a..b]) at out[off..]. 52func md_put_esc(out: *u8, cap: i64, off: i64, 53 src: *u8, a: i64, b: i64) -> i64 { 54 let w: i64 = html_escape(out + off, cap - off, src + a, b - a) 55 if w < 0 { return MD_ERR_SHORT } 56 return off + w 57} 58 59// Render inline markdown src[0..n] into out[0..]. Returns bytes 60// written. Parser is a single forward pass with peek-and-match 61// for delimiter pairs. 62func markdown_inline(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 63 var i: i64 = 0 64 var o: i64 = 0 65 var plain_start: i64 = 0 // start of current uninterrupted run 66 67 while i < n { 68 let c: i64 = src[i] 69 70 // Code span: backtick opens; find closing backtick. 71 if c == 0x60 { 72 // Flush pending plain text. 73 o = md_put_esc(out, cap, o, src, plain_start, i) 74 if o < 0 { return o } 75 // Find the matching backtick. 76 var j: i64 = i + 1 77 while j < n { 78 if src[j] == 0x60 { break } 79 j = j + 1 80 } 81 if j < n { 82 o = md_put(out, cap, o, "<code>", 6) 83 if o < 0 { return o } 84 o = md_put_esc(out, cap, o, src, i + 1, j) 85 if o < 0 { return o } 86 o = md_put(out, cap, o, "</code>", 7) 87 if o < 0 { return o } 88 i = j + 1 89 plain_start = i 90 continue 91 } 92 // Unterminated backtick: emit literal. 93 i = i + 1 94 continue 95 } 96 97 // Strong emphasis: ** or __ (same char doubled). 98 if c == 0x2A { 99 if i + 1 < n { 100 if src[i + 1] == 0x2A { 101 // find the closing ** 102 var j: i64 = i + 2 103 while j + 1 < n { 104 if src[j] == 0x2A { 105 if src[j + 1] == 0x2A { break } 106 } 107 j = j + 1 108 } 109 if j + 1 < n { 110 o = md_put_esc(out, cap, o, src, plain_start, i) 111 if o < 0 { return o } 112 o = md_put(out, cap, o, "<strong>", 8) 113 if o < 0 { return o } 114 o = md_put_esc(out, cap, o, src, i + 2, j) 115 if o < 0 { return o } 116 o = md_put(out, cap, o, "</strong>", 9) 117 if o < 0 { return o } 118 i = j + 2 119 plain_start = i 120 continue 121 } 122 } 123 } 124 // Single '*' emphasis. 125 var j: i64 = i + 1 126 while j < n { 127 if src[j] == 0x2A { break } 128 j = j + 1 129 } 130 if j < n { 131 o = md_put_esc(out, cap, o, src, plain_start, i) 132 if o < 0 { return o } 133 o = md_put(out, cap, o, "<em>", 4) 134 if o < 0 { return o } 135 o = md_put_esc(out, cap, o, src, i + 1, j) 136 if o < 0 { return o } 137 o = md_put(out, cap, o, "</em>", 5) 138 if o < 0 { return o } 139 i = j + 1 140 plain_start = i 141 continue 142 } 143 i = i + 1 144 continue 145 } 146 147 // Link: [label](href) 148 if c == 0x5B { 149 // Find closing ']' 150 var close_label: i64 = i + 1 151 while close_label < n { 152 if src[close_label] == 0x5D { break } 153 close_label = close_label + 1 154 } 155 if close_label < n { 156 if close_label + 1 < n { 157 if src[close_label + 1] == 0x28 { 158 // Find closing ')' 159 var close_url: i64 = close_label + 2 160 while close_url < n { 161 if src[close_url] == 0x29 { break } 162 close_url = close_url + 1 163 } 164 if close_url < n { 165 o = md_put_esc(out, cap, o, src, plain_start, i) 166 if o < 0 { return o } 167 o = md_put(out, cap, o, "<a href=\"", 9) 168 if o < 0 { return o } 169 o = md_put_esc(out, cap, o, src, 170 close_label + 2, close_url) 171 if o < 0 { return o } 172 o = md_put(out, cap, o, "\">", 2) 173 if o < 0 { return o } 174 o = md_put_esc(out, cap, o, src, 175 i + 1, close_label) 176 if o < 0 { return o } 177 o = md_put(out, cap, o, "</a>", 4) 178 if o < 0 { return o } 179 i = close_url + 1 180 plain_start = i 181 continue 182 } 183 } 184 } 185 } 186 } 187 188 i = i + 1 189 } 190 // Flush trailing plain text. 191 o = md_put_esc(out, cap, o, src, plain_start, n) 192 return o 193} 194 195// Compile-only smoke. 196func main() -> i64 { 197 let out: *u8 = sys_mmap(512) 198 199 // Plain + escape. 200 let n1: i64 = markdown_inline("a<b>c", 5, out, 512) 201 // Expected: "a&lt;b&gt;c" = 11 bytes. 202 if n1 != 11 { return 1 } 203 204 // Strong + em. 205 let n2: i64 = markdown_inline("**bold** and *em*", 17, out, 512) 206 if n2 <= 0 { return 2 } 207 // Must contain <strong> 208 var has_strong: i64 = 0 209 var i: i64 = 0 210 while i < n2 - 8 { 211 if out[i] == 0x3C { 212 if out[i + 1] == 0x73 { // 's' 213 if out[i + 2] == 0x74 { // 't' 214 has_strong = 1 215 break 216 } 217 } 218 } 219 i = i + 1 220 } 221 if has_strong != 1 { return 3 } 222 223 // Code span. 224 let n3: i64 = markdown_inline("use `printf(\"hi\")`", 18, out, 512) 225 if n3 <= 0 { return 4 } 226 227 // Link. 228 let n4: i64 = markdown_inline("[Nishi](https://nishifamily.com)", 32, 229 out, 512) 230 if n4 <= 0 { return 5 } 231 if out[0] != 0x3C { return 6 } // '<' 232 if out[1] != 0x61 { return 7 } // 'a' 233 234 return 0 235}