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<b>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}