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}