code wiki / (root) / nx_textcut.nx

nx_textcut.nx source

↩ module page · 232 lines · 10501 B

1// nx_textcut.nx -- LIB: TOKEN-BOUNDARY-SAFE TEXT CUTTING. The estate's ONE truncation ruler. 2// 3// THE DEFECT THIS EXISTS TO REMOVE, MEASURED LIVE ON nishifamily.com/search 2026-08-25: 4// a renderer that truncates at a BYTE cap cuts mid-token. The stored doc 5// "NIST Reference Fluid Thermodynamic and Transport Properties Database (REFPROP) Version 9" 6// rendered a title ending "...Database (RE" (72-byte cap) and a snippet BEGINNING "FPROP) Version 9". 7// TWO independent caps in one renderer (title 72B, sentence 320B) both cut mid-word. 8// 9// WHY IT SURVIVED: the damage is INVISIBLE AT THE CUT SITE. A title ending "(RE" looks merely 10// truncated -- plausible. The mid-word START it creates surfaces in the NEXT span, which reads as 11// a snippet bug, so every investigation looked at the snippet code and found nothing wrong with it. 12// A CUT IS A CLAIM ABOUT WHERE A TOKEN ENDS; this lib makes that claim checkable and never 13// silently wrong -- every cut lands on a real boundary or ANNOUNCES that it could not. 14// 15// ALSO CLOSES A DECLARED GAP ELSEWHERE: nx_search_snippet_extract.nx names both 16// "UTF-8 codepoint awareness (no mid-codepoint truncation)" and "Word-boundary clipping" in its 17// V2 SCOPE (TODO) and is unwired. Treating every byte >= 128 as a token byte closes BOTH at once: 18// a multi-byte codepoint can never be split, because a cut inside one is a cut inside a token. 19// 20// NO THRESHOLDS. This lib is pure STRUCTURE (what a token is), not POLICY (how much to show), so 21// it carries no conf and nothing here is tunable -- the cap is always the CALLER's. 22// license_tier: ORIGINAL 23import "nx_syscalls.nx" 24 25// ASCII class boundaries -- character DEFINITIONS, named per rule 11 (house pattern: nx_block_density BD_LT/BD_GT). 26const TC_CH_TAB: i64 = 9 27const TC_CH_LF: i64 = 10 28const TC_CH_CR: i64 = 13 29const TC_CH_SP: i64 = 32 30const TC_CH_D0: i64 = 48 31const TC_CH_D9: i64 = 57 32const TC_CH_UA: i64 = 65 33const TC_CH_UZ: i64 = 90 34const TC_CH_US: i64 = 95 35const TC_CH_LA: i64 = 97 36const TC_CH_LZ: i64 = 122 37const TC_CH_HIGH: i64 = 128 // first non-ASCII byte: lead or continuation of a UTF-8 codepoint 38// bytes that OPEN or CONNECT -- see tc_is_dangling. Named rather than spelled as a literal set 39// because the nx lexer forbids '#' inside a string literal, so a character-class STRING is not even 40// expressible here; consts are the only honest way to write this set down. 41const TC_CH_DQUOTE: i64 = 34 42const TC_CH_HASH: i64 = 35 43const TC_CH_AMP: i64 = 38 44const TC_CH_SQUOTE: i64 = 39 45const TC_CH_LPAREN: i64 = 40 46const TC_CH_PLUS: i64 = 43 47const TC_CH_COMMA: i64 = 44 48const TC_CH_HYPHEN: i64 = 45 49const TC_CH_SLASH: i64 = 47 50const TC_CH_LT: i64 = 60 51const TC_CH_EQ: i64 = 61 52const TC_CH_AT: i64 = 64 53const TC_CH_LBRACK: i64 = 91 54const TC_CH_LBRACE: i64 = 123 55const TC_CH_PIPE: i64 = 124 56 57// ANNOUNCE, NEVER INFER: a consumer that wants to report its own imprecision reads these 58// instead of assuming every cut was clean. (Same contract as nx_block_density's bd_last_* stats.) 59static tc_cuts_g: i64 60static tc_hardcuts_g: i64 61 62func tc_stats_reset() -> i64 { tc_cuts_g = 0; tc_hardcuts_g = 0; return 0 } 63func tc_cuts() -> i64 { return tc_cuts_g } 64func tc_hardcuts() -> i64 { return tc_hardcuts_g } 65 66// A TOKEN BYTE. Alphanumeric, underscore, or ANY byte >= 128. 67// The >= 128 arm is load-bearing and is not a shortcut: UTF-8 lead and continuation bytes are all 68// >= 128, so refusing to cut between two of them makes a mid-codepoint cut structurally impossible. 69func tc_is_wordch(c: i64) -> i64 { 70 if c >= TC_CH_D0 { if c <= TC_CH_D9 { return 1 } } 71 if c >= TC_CH_UA { if c <= TC_CH_UZ { return 1 } } 72 if c >= TC_CH_LA { if c <= TC_CH_LZ { return 1 } } 73 if c == TC_CH_US { return 1 } 74 if c >= TC_CH_HIGH { return 1 } 75 return 0 76} 77 78func tc_is_ws(c: i64) -> i64 { 79 if c == TC_CH_SP { return 1 } 80 if c == TC_CH_LF { return 1 } 81 if c == TC_CH_CR { return 1 } 82 if c == TC_CH_TAB { return 1 } 83 return 0 84} 85 86// A UTF-8 CONTINUATION BYTE (10xxxxxx = 128..191). A cut landing ON one of these is a cut INSIDE a 87// codepoint, which mangles the final character. The token rule above normally makes this impossible, 88// but it CANNOT when the whole span is one unbroken token -- and that is exactly the hard-cut path. 89// Found by this lib's own gate on its first run: the fixture 'a','a',0xC3,0xA9,'a' at cap 3 has no 90// token boundary at all, so the hard cut returned cap and split the codepoint. 91const TC_CH_CONT_HI: i64 = 191 92func tc_is_utf8_cont(c: i64) -> i64 { 93 if c >= TC_CH_HIGH { if c <= TC_CH_CONT_HI { return 1 } } 94 return 0 95} 96 97// THE PREDICATE EVERYTHING ELSE IS BUILT ON: would cutting [0,n) at `at` split a token in half? 98// Cutting at 0 or at n is never a split -- those are the ends of the span, not a position inside a token. 99func tc_splits_token(txt: *u8, n: i64, at: i64) -> i64 { 100 if at <= 0 { return 0 } 101 if at >= n { return 0 } 102 if tc_is_wordch(txt[at - 1] as i64) == 0 { return 0 } 103 if tc_is_wordch(txt[at] as i64) == 0 { return 0 } 104 return 1 105} 106 107// A DANGLING BYTE: one that opens or connects and therefore cannot be the last byte of a span that 108// was TRUNCATED. Cutting "...Database (REFPROP)" at the longest non-splitting boundary yields 109// "...Database (" -- not mid-word, but still visibly broken. These are trimmed ONLY when the cut 110// actually truncated: an author's own trailing '.' or ')' on a span that fit is left exactly alone. 111func tc_is_dangling(c: i64) -> i64 { 112 if c == TC_CH_LPAREN { return 1 } 113 if c == TC_CH_LBRACK { return 1 } 114 if c == TC_CH_LBRACE { return 1 } 115 if c == TC_CH_LT { return 1 } 116 if c == TC_CH_DQUOTE { return 1 } 117 if c == TC_CH_SQUOTE { return 1 } 118 if c == TC_CH_HYPHEN { return 1 } 119 if c == TC_CH_SLASH { return 1 } 120 if c == TC_CH_AMP { return 1 } 121 if c == TC_CH_AT { return 1 } 122 if c == TC_CH_HASH { return 1 } 123 if c == TC_CH_EQ { return 1 } 124 if c == TC_CH_PLUS { return 1 } 125 if c == TC_CH_COMMA { return 1 } 126 if c == TC_CH_PIPE { return 1 } 127 return 0 128} 129 130// THE CUT. Returns the LONGEST length L <= min(n, cap) that does not split a token. 131// Longest, not prettiest: a rule that walked back to the previous SPACE would be simpler but would 132// throw away up to a whole cap's worth of text on a run with no spaces in it, and a shorter answer 133// is not a safer one -- it is just less of the document. 134// exactbox[0] = 1 -> the cut landed on a real boundary. 135// exactbox[0] = 0 -> the span is ONE unbroken token longer than cap (a 300-char URL, a hash, a 136// CJK run) and the cut HAD to be hard. DECLARED, never hidden: a caller that 137// cares can render an ellipsis, and the counters above make the rate measurable. 138func tc_cut(txt: *u8, n: i64, cap: i64, exactbox: *i64) -> i64 { 139 exactbox[0] = 1 140 if cap <= 0 { return 0 } 141 if n <= cap { return n } 142 tc_cuts_g = tc_cuts_g + 1 143 var l: i64 = cap 144 var go: i64 = 1 145 while go == 1 { 146 if l <= 0 { go = 0 } else { 147 if tc_splits_token(txt, n, l) == 0 { return l } 148 l = l - 1 149 } 150 } 151 // HARD CUT: no token boundary exists anywhere in [1, cap], so we MUST cut inside a token. We must 152 // still never cut inside a CHARACTER: back off any continuation byte so the cut lands on a codepoint 153 // boundary. This is a strictly WEAKER guarantee than the token rule, and it is declared as such -- 154 // exactbox stays 0 and the hardcut counter still fires, because the caller's text really was severed. 155 var hl: i64 = cap 156 var hg: i64 = 1 157 while hg == 1 { 158 if hl <= 0 { hg = 0 } else { 159 if tc_is_utf8_cont(txt[hl] as i64) == 1 { hl = hl - 1 } else { hg = 0 } 160 } 161 } 162 if hl <= 0 { hl = cap } 163 exactbox[0] = 0 164 tc_hardcuts_g = tc_hardcuts_g + 1 165 return hl 166} 167 168// tc_cut, then -- ONLY IF IT TRUNCATED -- drop trailing whitespace and dangling opener/connector 169// bytes. The truncation test is n > cap, checked BEFORE the cut, so a span that fit is returned 170// byte-for-byte and this function is a no-op on it. 171func tc_cut_trim(txt: *u8, n: i64, cap: i64, exactbox: *i64) -> i64 { 172 var truncated: i64 = 0 173 if n > cap { truncated = 1 } 174 var l: i64 = tc_cut(txt, n, cap, exactbox) 175 var go: i64 = 1 176 while go == 1 { 177 if l <= 0 { go = 0 } else { 178 let c: i64 = txt[l - 1] as i64 179 var drop: i64 = 0 180 if tc_is_ws(c) == 1 { drop = 1 } 181 if drop == 0 { if truncated == 1 { if tc_is_dangling(c) == 1 { drop = 1 } } } 182 if drop == 1 { l = l - 1 } else { go = 0 } 183 } 184 } 185 return l 186} 187 188// THE OTHER HALF OF THE DEFECT. Advance `off` to the start of a whole token: when off lands INSIDE 189// a token -- because some earlier cap cut there -- skip the remainder of that token, then skip 190// whitespace. This is what turns a snippet reading "FPROP) Version 9" into "Version 9". 191// Uses an explicit flag to leave each loop; never writes the exit sentinel into the cursor itself 192// (that idiom erases the answer, and nx_srclint hunts it). 193func tc_start(txt: *u8, n: i64, off: i64) -> i64 { 194 var i: i64 = off 195 if i < 0 { i = 0 } 196 if i >= n { return n } 197 var wassplit: i64 = 0 198 if tc_splits_token(txt, n, i) == 1 { wassplit = 1 } 199 if wassplit == 1 { 200 // skip the amputated remainder of the token we landed inside ("FPROP") 201 var g1: i64 = 1 202 while g1 == 1 { 203 if i >= n { g1 = 0 } else { 204 if tc_is_wordch(txt[i] as i64) == 1 { i = i + 1 } else { g1 = 0 } 205 } 206 } 207 // ...and then the punctuation that amputation ORPHANED (the ')' of a '(...)' whose '(' was cut 208 // away, giving ") Version 9"). This runs ONLY on the split path -- a span that began cleanly 209 // keeps its own leading quote or bracket, which is content and not an artifact. 210 var g2: i64 = 1 211 while g2 == 1 { 212 if i >= n { g2 = 0 } else { 213 if tc_is_wordch(txt[i] as i64) == 1 { g2 = 0 } else { i = i + 1 } 214 } 215 } 216 return i 217 } 218 return tc_skip_ws(txt, n, i) 219} 220 221// skip whitespace only (no token logic) -- the common prefix trim. 222func tc_skip_ws(txt: *u8, n: i64, off: i64) -> i64 { 223 var i: i64 = off 224 if i < 0 { i = 0 } 225 var go: i64 = 1 226 while go == 1 { 227 if i >= n { go = 0 } else { 228 if tc_is_ws(txt[i] as i64) == 1 { i = i + 1 } else { go = 0 } 229 } 230 } 231 return i 232}