code wiki / _hdl_build / nx_browser_forms.nx
nx_browser_forms.nx source
↩ module page · 325 lines · 15832 B
1// nx_browser_forms.nx -- the SOVEREIGN Nishi-browser FORM model: parse <form>/<input>/<textarea> from a
2// rendered page, hold each field's EDITABLE value, and on submit produce the exact HTTP request
3// (method + action + application/x-www-form-urlencoded body). This is the load-bearing browser-core rung
4// that makes the nishi-first no-JS portals (docportal/mail/siteedit login + editor) ACTUALLY OPERABLE in
5// the Nishi browser (operator 2026-07-05 doctrine: nishi os/browser FIRST). PURE bytes-in/values-out --
6// no X11, no paint -- so it is fully gate-provable (nx_browser_forms_gate); the render (draw input boxes +
7// caret) and the live keyboard/click wiring COMPOSE this core. Reuses the shipped HTML tokenizer + DOM
8// attr finder (rule 15 DRY, same as br_extract). license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_html_tokenizer.nx"
11import "nx_dom_query.nx"
12const BF_MAGIC_65536: i64 = 65536
13const BF_MAGIC_4096: i64 = 4096
14const BF_MAGIC_131072: i64 = 131072
15
16const BF_K_TEXT: i64 = 0 // <input> text/other
17const BF_K_HIDDEN: i64 = 1 // <input type=hidden>
18const BF_K_TEXTAREA: i64 = 2 // <textarea>
19const BF_K_PASSWORD: i64 = 3 // <input type=password>
20
21struct BrField {
22 name: *u8, // NUL-terminated field name (copied out of the HTML)
23 name_len: i64,
24 kind: i64, // BF_K_*
25 val: *u8, // mutable value buffer (the user types into this)
26 val_len: i64,
27 val_cap: i64,
28 fx: i64, // synthetic layout rect (bf_layout_fields); fh=0 => not shown / not hittable (hidden)
29 fy: i64,
30 fw: i64,
31 fh: i64
32}
33const NX_BRFIELD_BYTES: i64 = 80
34
35struct BrForm {
36 action: *u8, // NUL-terminated action URL/path
37 action_len: i64,
38 method_post: i64, // 1 = POST, 0 = GET (default)
39 field_start: i64, // index into the fields[] arena
40 field_count: i64,
41 sx: i64, // synthetic submit-button rect (bf_layout_fields places it below the fields)
42 sy: i64,
43 sw: i64,
44 sh: i64
45}
46const NX_BRFORM_BYTES: i64 = 72
47
48func bf_field(fields: *BrField, i: i64) -> *BrField { return ((fields as i64) + i * NX_BRFIELD_BYTES) as *BrField }
49func bf_form(forms: *BrForm, i: i64) -> *BrForm { return ((forms as i64) + i * NX_BRFORM_BYTES) as *BrForm }
50
51// copy html[off..off+len) into a fresh NUL-terminated buffer.
52func bf_copy(html: *u8, off: i64, len: i64) -> *u8 {
53 let b: *u8 = sys_mmap(len + 4)
54 var i: i64 = 0
55 while i < len { b[i] = html[off + i]; i = i + 1 }
56 b[len] = 0 as u8
57 return b
58}
59// ascii-lowercase byte equality of html[off..off+len) vs a NUL lit
60func bf_ci_eq(html: *u8, off: i64, len: i64, lit: *u8) -> i64 {
61 var ll: i64 = 0
62 while lit[ll] != (0 as u8) { ll = ll + 1 }
63 if len != ll { return 0 }
64 var i: i64 = 0
65 while i < len {
66 var a: i64 = html[off + i] as i64
67 if a >= 65 { if a <= 90 { a = a + 32 } }
68 if a != (lit[i] as i64) { return 0 }
69 i = i + 1
70 }
71 return 1
72}
73
74// parse all forms + fields from `html`. Fills forms[]/fields[] arenas; returns form count. Fields get a
75// fresh editable buffer seeded with any value= (hidden/text) or textarea inner text. maxf/maxfld cap.
76func bf_parse(html: *u8, hlen: i64, forms: *BrForm, maxf: i64, fields: *BrField, maxfld: i64) -> i64 {
77 let c: *HtmlCursor = sys_mmap(24) as *HtmlCursor
78 nx_html_cursor_init(c, html, hlen)
79 let tok: *HtmlToken = sys_mmap(56) as *HtmlToken
80 let ao: *i64 = sys_mmap(8) as *i64
81 let al: *i64 = sys_mmap(8) as *i64
82 let bo: *i64 = sys_mmap(8) as *i64
83 let bl: *i64 = sys_mmap(8) as *i64
84 var nf: i64 = 0 // form count
85 var nfld: i64 = 0 // field count
86 var curform: i64 = 0 - 1 // index of the open form, or -1
87 var pending_ta: i64 = 0 - 1 // field idx awaiting its textarea inner text
88 while 1 == 1 {
89 nx_html_next_token(c, tok)
90 if tok.kind == NX_HTML_TOK_EOF { return nf }
91 let is_start: i64 = (tok.kind == NX_HTML_TOK_START_TAG) as i64
92 let is_self: i64 = (tok.kind == NX_HTML_TOK_SELF_CLOSING) as i64
93 // textarea inner text
94 if tok.kind == NX_HTML_TOK_TEXT {
95 if pending_ta >= 0 {
96 let fld: *BrField = bf_field(fields, pending_ta)
97 var w: i64 = 0
98 var i: i64 = 0
99 while i < tok.body_len { if w < fld.val_cap - 1 { fld.val[w] = html[tok.body_off + i]; w = w + 1 } i = i + 1 }
100 fld.val_len = w
101 pending_ta = 0 - 1
102 }
103 }
104 if is_start == 1 { if pending_ta >= 0 { pending_ta = 0 - 1 } } // empty textarea; stop waiting
105 if is_start == 1 {
106 if bf_ci_eq(html, tok.name_off, tok.name_len, "form\x00" as *u8) == 1 {
107 if nf < maxf {
108 let fm: *BrForm = bf_form(forms, nf)
109 fm.action = "\x00" as *u8
110 fm.action_len = 0
111 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "action\x00" as *u8, ao, al) == 1 { fm.action = bf_copy(html, ao[0], al[0]); fm.action_len = al[0] }
112 fm.method_post = 0
113 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "method\x00" as *u8, bo, bl) == 1 { if bf_ci_eq(html, bo[0], bl[0], "post\x00" as *u8) == 1 { fm.method_post = 1 } }
114 fm.field_start = nfld
115 fm.field_count = 0
116 curform = nf
117 nf = nf + 1
118 }
119 }
120 if bf_ci_eq(html, tok.name_off, tok.name_len, "textarea\x00" as *u8) == 1 {
121 if curform >= 0 { if nfld < maxfld {
122 let fld: *BrField = bf_field(fields, nfld)
123 fld.name = "\x00" as *u8; fld.name_len = 0
124 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "name\x00" as *u8, ao, al) == 1 { fld.name = bf_copy(html, ao[0], al[0]); fld.name_len = al[0] }
125 fld.kind = BF_K_TEXTAREA
126 fld.val = sys_mmap(BF_MAGIC_65536); fld.val_len = 0; fld.val_cap = BF_MAGIC_65536
127 pending_ta = nfld
128 let fm2: *BrForm = bf_form(forms, curform)
129 fm2.field_count = fm2.field_count + 1
130 nfld = nfld + 1
131 } }
132 }
133 }
134 // <input> may be START or SELF_CLOSING
135 var is_input: i64 = 0
136 if is_start == 1 { if bf_ci_eq(html, tok.name_off, tok.name_len, "input\x00" as *u8) == 1 { is_input = 1 } }
137 if is_self == 1 { if bf_ci_eq(html, tok.name_off, tok.name_len, "input\x00" as *u8) == 1 { is_input = 1 } }
138 if is_input == 1 {
139 if curform >= 0 { if nfld < maxfld {
140 // a name-less input (e.g. a submit button styled as input) is not a data field
141 var has_name: i64 = 0
142 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "name\x00" as *u8, ao, al) == 1 { has_name = 1 }
143 if has_name == 1 {
144 let fld: *BrField = bf_field(fields, nfld)
145 fld.name = bf_copy(html, ao[0], al[0]); fld.name_len = al[0]
146 var kind: i64 = BF_K_TEXT
147 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "type\x00" as *u8, bo, bl) == 1 {
148 if bf_ci_eq(html, bo[0], bl[0], "hidden\x00" as *u8) == 1 { kind = BF_K_HIDDEN }
149 if bf_ci_eq(html, bo[0], bl[0], "password\x00" as *u8) == 1 { kind = BF_K_PASSWORD }
150 }
151 fld.kind = kind
152 fld.val = sys_mmap(BF_MAGIC_4096); fld.val_len = 0; fld.val_cap = BF_MAGIC_4096
153 // seed from value= (hidden fields carry data; text/password start from any default)
154 if nx_dom_find_attr(html, tok.src_off, tok.src_len, "value\x00" as *u8, ao, al) == 1 {
155 var w: i64 = 0
156 var i: i64 = 0
157 while i < al[0] { if w < fld.val_cap - 1 { fld.val[w] = html[ao[0] + i]; w = w + 1 } i = i + 1 }
158 fld.val_len = w
159 }
160 let fm3: *BrForm = bf_form(forms, curform)
161 fm3.field_count = fm3.field_count + 1
162 nfld = nfld + 1
163 }
164 } }
165 }
166 if tok.kind == NX_HTML_TOK_END_TAG {
167 if bf_ci_eq(html, tok.name_off, tok.name_len, "form\x00" as *u8) == 1 { curform = 0 - 1 }
168 }
169 }
170 return nf
171}
172
173// ---- synthetic form LAYOUT (decoupled from the CSS engine): stack the VISIBLE fields of form `fi` at
174// (x0,y0), each `fw` wide + `rh` tall with a 6px gap. Hidden fields get fh=0 (not shown, not hittable).
175// The browser paints each field with bf_paint_field(fb, fld.fx, fld.fy, fld.fw, fld.fh, ...) and focuses
176// via br_hit_input -- SAME rects, so paint and click agree by construction. Returns total height used.
177func bf_layout_fields(forms: *BrForm, fi: i64, fields: *BrField, x0: i64, y0: i64, fw: i64, rh: i64) -> i64 {
178 let fm: *BrForm = bf_form(forms, fi)
179 var y: i64 = y0
180 var k: i64 = 0
181 while k < fm.field_count {
182 let idx: i64 = fm.field_start + k
183 let fld: *BrField = bf_field(fields, idx)
184 if fld.kind == BF_K_HIDDEN { fld.fx = 0; fld.fy = 0; fld.fw = 0; fld.fh = 0 }
185 else {
186 var rr: i64 = rh
187 if fld.kind == BF_K_TEXTAREA { rr = rh * 5 } // textareas are taller
188 fld.fx = x0; fld.fy = y; fld.fw = fw; fld.fh = rr
189 y = y + rr + 6
190 }
191 k = k + 1
192 }
193 // a submit button below the fields (so a no-JS click has a target); ~120 wide, one row tall.
194 fm.sx = x0
195 fm.sy = y
196 fm.sw = 120
197 fm.sh = rh
198 y = y + rh + 6
199 return y - y0
200}
201
202// hit-test a click against form `fi`'s SUBMIT button rect; 1 = submit, else 0. (The browser then calls
203// bf_submit_request + writes it to the socket.)
204func br_hit_submit(forms: *BrForm, fi: i64, dx: i64, dy: i64) -> i64 {
205 let fm: *BrForm = bf_form(forms, fi)
206 if fm.sh > 0 { if dx >= fm.sx { if dx < fm.sx + fm.sw { if dy >= fm.sy { if dy < fm.sy + fm.sh { return 1 } } } } }
207 return 0
208}
209
210// hit-test a click (dx,dy) against form `fi`'s laid-out field rects; returns the field INDEX or -1.
211// Mirror of br_hit_link, but for the synthetic form rects (hidden fields fh=0 are never hit).
212func br_hit_input(forms: *BrForm, fi: i64, fields: *BrField, dx: i64, dy: i64) -> i64 {
213 let fm: *BrForm = bf_form(forms, fi)
214 var k: i64 = 0
215 while k < fm.field_count {
216 let idx: i64 = fm.field_start + k
217 let fld: *BrField = bf_field(fields, idx)
218 if fld.fh > 0 {
219 if dx >= fld.fx { if dx < fld.fx + fld.fw { if dy >= fld.fy { if dy < fld.fy + fld.fh { return idx } } } }
220 }
221 k = k + 1
222 }
223 return 0 - 1
224}
225
226// ---- editing: the keyboard layer calls these on the FOCUSED field ----
227func bf_type(fld: *BrField, ch: i64) -> i64 { if fld.val_len < fld.val_cap - 1 { fld.val[fld.val_len] = ch as u8; fld.val_len = fld.val_len + 1 } return 0 }
228func bf_backspace(fld: *BrField) -> i64 { if fld.val_len > 0 { fld.val_len = fld.val_len - 1 } return 0 }
229// convenience for tests/paste: type a whole NUL-terminated string
230func bf_set(fld: *BrField, s: *u8) -> i64 { fld.val_len = 0; var i: i64 = 0; while s[i] != (0 as u8) { bf_type(fld, s[i] as i64); i = i + 1 } return 0 }
231
232// x-www-form-urlencode src[0..n) into out at offset o; returns new offset. Unreserved (RFC3986) pass;
233// space -> %20; everything else -> %XX (matches what a server's sd_urldecode expects).
234func bf_urlenc(src: *u8, n: i64, out: *u8, o: i64) -> i64 {
235 let hx: *u8 = "0123456789ABCDEF" as *u8
236 var w: i64 = o
237 var i: i64 = 0
238 while i < n {
239 let c: i64 = src[i] as i64
240 var un: i64 = 0
241 if c >= 48 { if c <= 57 { un = 1 } }
242 if c >= 65 { if c <= 90 { un = 1 } }
243 if c >= 97 { if c <= 122 { un = 1 } }
244 if c == 45 { un = 1 }
245 if c == 46 { un = 1 }
246 if c == 95 { un = 1 }
247 if c == 126 { un = 1 }
248 if un == 1 { out[w] = c as u8; w = w + 1 }
249 else {
250 let hi: i64 = (c >> 4) & 15
251 let lo: i64 = c & 15
252 out[w] = 37 as u8
253 out[w+1] = hx[hi]
254 out[w+2] = hx[lo]
255 w = w + 3
256 }
257 i = i + 1
258 }
259 return w
260}
261
262// build the submit body for form `fi` into out (application/x-www-form-urlencoded); returns body length.
263func bf_submit_body(forms: *BrForm, fi: i64, fields: *BrField, out: *u8) -> i64 {
264 let fm: *BrForm = bf_form(forms, fi)
265 var o: i64 = 0
266 var k: i64 = 0
267 while k < fm.field_count {
268 let idx: i64 = fm.field_start + k
269 let fld: *BrField = bf_field(fields, idx)
270 if k > 0 { out[o] = 38 as u8; o = o + 1 } // '&'
271 o = bf_urlenc(fld.name, fld.name_len, out, o)
272 out[o] = 61 as u8; o = o + 1 // '='
273 o = bf_urlenc(fld.val, fld.val_len, out, o)
274 k = k + 1
275 }
276 return o
277}
278
279func bf_cats(out: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){out[o+i]=s[i];i=i+1} return o+i }
280func bf_catn(out: *u8, o: i64, v: i64) -> i64 { if v==0 {out[o]=48 as u8; return o+1} let t: *u8=sys_mmap(24); var m: i64=v; var k: i64=0; while m>0 {t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k {out[o+i]=t[k-1-i];i=i+1} return o+k }
281
282// build the FULL HTTP request bytes for submitting form `fi` to `host` (POST = form-urlencoded body;
283// GET = body appended to the action as a query). The action keeps any existing ?query (e.g. the
284// nishi-first ?s= session token rides here). This is what the browser writes to the TLS socket on a
285// submit-button click; the response then loads as a normal navigation. Returns request length.
286func bf_submit_request(host: *u8, hostlen: i64, forms: *BrForm, fi: i64, fields: *BrField, out: *u8) -> i64 {
287 let fm: *BrForm = bf_form(forms, fi)
288 let bodybuf: *u8 = sys_mmap(BF_MAGIC_131072)
289 let blen: i64 = bf_submit_body(forms, fi, fields, bodybuf)
290 var o: i64 = 0
291 if fm.method_post == 1 { o = bf_cats(out, o, "POST " as *u8) } else { o = bf_cats(out, o, "GET " as *u8) }
292 var i: i64 = 0
293 while i < fm.action_len { out[o] = fm.action[i]; o = o + 1; i = i + 1 }
294 if fm.method_post == 0 { out[o] = 63 as u8; o = o + 1; var j: i64 = 0; while j < blen { out[o] = bodybuf[j]; o = o + 1; j = j + 1 } }
295 o = bf_cats(out, o, " HTTP/1.1\r\nHost: " as *u8)
296 i = 0
297 while i < hostlen { out[o] = host[i]; o = o + 1; i = i + 1 }
298 o = bf_cats(out, o, "\r\n" as *u8)
299 if fm.method_post == 1 {
300 o = bf_cats(out, o, "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
301 o = bf_catn(out, o, blen)
302 o = bf_cats(out, o, "\r\nConnection: close\r\n\r\n" as *u8)
303 var k: i64 = 0
304 while k < blen { out[o] = bodybuf[k]; o = o + 1; k = k + 1 }
305 } else { o = bf_cats(out, o, "Connection: close\r\n\r\n" as *u8) }
306 return o
307}
308
309// tiny self-check so the module builds+runs standalone (real proof = nx_browser_forms_gate).
310func main() -> i64 {
311 let html: *u8 = "<form method=post action=/x><input name=a value=1><input name=b></form>" as *u8
312 var hl: i64 = 0
313 while html[hl] != (0 as u8) { hl = hl + 1 }
314 let forms: *BrForm = sys_mmap(NX_BRFORM_BYTES * 8) as *BrForm
315 let fields: *BrField = sys_mmap(NX_BRFIELD_BYTES * 32) as *BrField
316 let nf: i64 = bf_parse(html, hl, forms, 8, fields, 32)
317 let out: *u8 = sys_mmap(BF_MAGIC_4096)
318 let bl: i64 = bf_submit_body(forms, 0, fields, out)
319 sys_write(1, "nx_browser_forms self: forms=" as *u8, 29)
320 let d: *u8 = sys_mmap(8); d[0] = (48 + nf) as u8; sys_write(1, d, 1)
321 sys_write(1, " body=" as *u8, 6)
322 sys_write(1, out, bl)
323 sys_write(1, "\n" as *u8, 1)
324 return 0
325}