nx_doc_extractor_md.nx source
↩ module page · 153 lines · 5572 B
1// nx_doc_extractor_md.nx -- V2.0 P-7: deep .md artifact parser.
2//
3// Reads .md body bytes; extracts heading_count (## §N) + code_block_count
4// (fenced blocks) for populating substrate signal richer than the V2.0
5// composes_count alone.
6//
7// COMPOSES:
8// nx_html_extract.nx_he_match_ci (re-used by P-6)
9//
10// COMPOSED BY:
11// wiki/nx_pipeline_walker (V3+ swap; V2.0 calls separately via main)
12//
13// V2.0 P-7 SCOPE:
14// - Count "## " or "### " or higher-depth heading line-starts -> heading_count
15// - Count code-fence pairs (```lang ... ```) -> code_block_count
16// - Bounded; line-walker; pure-function
17//
18// V3+ SCOPE (TODO):
19// - Identify [N]/[I]/[R] paragraph tags per NISHIDOC_STANDARD §3
20// - Extract section TOC (§N + title)
21// - Detect S-class doc structure (frontmatter + numbered sections)
22// - Count footnotes + references
23//
24// Status: V2.0 P-7. 2026-05-27.
25
26import "nx_syscalls.nx"
27import "nx_html_extract.nx"
28
29// ===== Sealed verdict surface (codes 3030-3034) =================================================
30const NX_DXMD_OK: i64 = 0
31const NX_DXMD_BAD_INPUT: i64 = 3030
32const NX_DXMD_LOOP_BUDGET: i64 = 3031
33
34// ===== Named constants (M7) =================================================
35const NX_DXMD_MAX_BODY: i64 = 4194304 // 4 MB cap (some specs are large)
36const NX_DXMD_LOOP_BUDGET_CAP: i64 = 10000000
37
38const NX_DXMD_ASCII_NL: i64 = 0x0A
39const NX_DXMD_ASCII_HASH: i64 = 0x23
40const NX_DXMD_ASCII_BACKTICK: i64 = 0x60
41
42// ===== Line-start: is this a Markdown heading? =================================================
43//
44// Returns 1 if line at line_off (after optional leading whitespace)
45// starts with one or more '#' followed by a space.
46
47func nx_dxmd_line_is_heading(body: *u8, line_off: i64, body_n: i64) -> i64 {
48 if line_off < 0 { return 0 }
49 if line_off >= body_n { return 0 }
50 // Skip whitespace
51 var p: i64 = line_off
52 while p < body_n {
53 let c: i64 = body[p] as i64
54 if c == 0x20 { p = p + 1 }
55 if c == 0x09 { p = p + 1 }
56 if c != 0x20 { if c != 0x09 { p = body_n + 1 } }
57 }
58 if p > body_n { p = body_n }
59 if p >= body_n { return 0 }
60 if (body[p] as i64) != NX_DXMD_ASCII_HASH { return 0 }
61 // Consume any number of '#' chars
62 var q: i64 = p
63 while q < body_n {
64 if (body[q] as i64) == NX_DXMD_ASCII_HASH { q = q + 1 }
65 if q < body_n {
66 if (body[q] as i64) != NX_DXMD_ASCII_HASH { q = body_n + 1 }
67 }
68 }
69 if q > body_n { q = body_n }
70 if q >= body_n { return 0 }
71 // After # chars, expect a space
72 if (body[q] as i64) == 0x20 { return 1 }
73 return 0
74}
75
76// ===== Line-start: is this a code fence (```)? =================================================
77//
78// Returns 1 if line begins with ``` (after optional leading whitespace).
79
80func nx_dxmd_line_is_fence(body: *u8, line_off: i64, body_n: i64) -> i64 {
81 if line_off < 0 { return 0 }
82 if line_off >= body_n { return 0 }
83 var p: i64 = line_off
84 while p < body_n {
85 let c: i64 = body[p] as i64
86 if c == 0x20 { p = p + 1 }
87 if c == 0x09 { p = p + 1 }
88 if c != 0x20 { if c != 0x09 { p = body_n + 1 } }
89 }
90 if p > body_n { p = body_n }
91 if p + 2 >= body_n { return 0 }
92 if (body[p] as i64) == NX_DXMD_ASCII_BACKTICK {
93 if (body[p + 1] as i64) == NX_DXMD_ASCII_BACKTICK {
94 if (body[p + 2] as i64) == NX_DXMD_ASCII_BACKTICK { return 1 }
95 }
96 }
97 return 0
98}
99
100// ===== Walk body counting headings + code blocks =================================================
101//
102// heading_count = total # of "## ..." / "### ..." / etc lines
103// code_block_count = # of ```...``` PAIRS (open + close); /2 the fence count
104
105func nx_doc_extract_md_stats(body: *u8, body_n: i64,
106 out_headings: *i64, out_code_blocks: *i64) -> i64 {
107 if (body as i64) == 0 { return 0 - NX_DXMD_BAD_INPUT }
108 if (out_headings as i64) == 0 { return 0 - NX_DXMD_BAD_INPUT }
109 if (out_code_blocks as i64) == 0 { return 0 - NX_DXMD_BAD_INPUT }
110 if body_n < 0 { return 0 - NX_DXMD_BAD_INPUT }
111 if body_n > NX_DXMD_MAX_BODY { return 0 - NX_DXMD_BAD_INPUT }
112 out_headings[0] = 0
113 out_code_blocks[0] = 0
114 if body_n < 1 { return NX_DXMD_OK }
115
116 var headings: i64 = 0
117 var fences: i64 = 0
118 var inside_code: i64 = 0
119
120 var i: i64 = 0
121 var iter: i64 = 0
122 var line_start: i64 = 0
123 while i <= body_n {
124 if iter >= NX_DXMD_LOOP_BUDGET_CAP { return 0 - NX_DXMD_LOOP_BUDGET }
125 iter = iter + 1
126 let at_end: i64 = if i == body_n then 1 else 0
127 var is_eol: i64 = at_end
128 if at_end == 0 {
129 if (body[i] as i64) == NX_DXMD_ASCII_NL { is_eol = 1 }
130 }
131 if is_eol == 1 {
132 // Fence (track open/close toggle so we don't count headings inside code)
133 if nx_dxmd_line_is_fence(body, line_start, body_n) == 1 {
134 fences = fences + 1
135 if inside_code == 0 { inside_code = 1 }
136 if inside_code == 1 { if (fences % 2) == 0 { inside_code = 0 } }
137 }
138 // Heading (only outside code blocks)
139 if inside_code == 0 {
140 if nx_dxmd_line_is_heading(body, line_start, body_n) == 1 {
141 headings = headings + 1
142 }
143 }
144 line_start = i + 1
145 }
146 i = i + 1
147 }
148
149 out_headings[0] = headings
150 // Code block count = fences / 2 (pair open + close)
151 out_code_blocks[0] = fences / 2
152 return NX_DXMD_OK
153}