@sema-agent/client-core 0.83.3 → 0.83.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,13 +37,51 @@ function applyEdits(text, edits) {
37
37
  }
38
38
  class PlainTarget {
39
39
  text;
40
- constructor(text) {
40
+ origin;
41
+ trace;
42
+ constructor(text, trace = false) {
41
43
  this.text = text;
44
+ this.trace = trace;
42
45
  }
43
46
  apply(edits) {
47
+ if (this.trace && edits.length > 0)
48
+ this.origin = editedOrigin(this.origin, this.text.length, edits);
44
49
  this.text = applyEdits(this.text, edits);
45
50
  }
46
51
  }
52
+ const traced = (text, origin) => (origin === undefined ? { text } : { text, origin });
53
+ function editedOrigin(prev, length, edits) {
54
+ let n = length;
55
+ for (const e of edits)
56
+ n += e.text.length - (e.end - e.start);
57
+ const out = new Int32Array(n);
58
+ let w = 0;
59
+ let cursor = 0;
60
+ const copy = (a, b) => {
61
+ for (let k = a; k < b; k++)
62
+ out[w++] = prev === undefined ? k : (prev[k] ?? -1);
63
+ };
64
+ for (const e of edits) {
65
+ copy(cursor, e.start);
66
+ out.fill(-1, w, w + e.text.length);
67
+ w += e.text.length;
68
+ cursor = e.end;
69
+ }
70
+ copy(cursor, length);
71
+ return out;
72
+ }
73
+ function composeOrigin(outer, inner) {
74
+ if (inner === undefined)
75
+ return outer;
76
+ if (outer === undefined)
77
+ return inner;
78
+ const out = new Int32Array(inner.length);
79
+ for (let k = 0; k < inner.length; k++) {
80
+ const s = inner[k] ?? -1;
81
+ out[k] = s < 0 ? -1 : (outer[s] ?? -1);
82
+ }
83
+ return out;
84
+ }
47
85
  class TrackedTarget {
48
86
  text;
49
87
  src;
@@ -272,9 +310,10 @@ function urlTokenEdits(text) {
272
310
  const ch = text.charAt(end);
273
311
  if (isWhitespaceOrControl(ch) || TOKEN_STOP.has(ch))
274
312
  break;
275
- if (ch === VIEW_SEPARATOR) {
313
+ const c = ch.charCodeAt(0);
314
+ if (ch === VIEW_SEPARATOR || isUnicodeBlank(c)) {
276
315
  let p = end;
277
- while (p < text.length && text[p] === VIEW_SEPARATOR)
316
+ while (p < text.length && (text[p] === VIEW_SEPARATOR || isUnicodeBlank(text.charCodeAt(p))))
278
317
  p++;
279
318
  if (credentialWordStartsNear(text, p))
280
319
  break;
@@ -317,6 +356,10 @@ function schemelessUserinfoEdits(text) {
317
356
  let runStart = at;
318
357
  while (runStart > floor && !breaksUserinfo(text.charCodeAt(runStart - 1)))
319
358
  runStart--;
359
+ if (text.charCodeAt(runStart) === 0x3a && runStart > floor && text[runStart - 1] === VIEW_SEPARATOR) {
360
+ while (runStart > floor && text[runStart - 1] === VIEW_SEPARATOR)
361
+ runStart--;
362
+ }
320
363
  let found = -1;
321
364
  let colon = -1;
322
365
  let s = runStart;
@@ -363,8 +406,23 @@ function washUrlNets(target) {
363
406
  return;
364
407
  }
365
408
  }
366
- const SECRET_SCHEME_WORD = /(?:\b|(?<=\\[A-Za-z"']))(bearer|basic)[\s:=\uFF1A\uFF1D]+/gi;
367
- const SECRET_LABELLED_WORD = /(?:\b|(?<=_)|(?<=\\[A-Za-z"']))((?:access[-_ ]?|refresh[-_ ]?|id[-_ ]?|client[-_ ]?|api[-_ ]?|x[-_]api[-_ ]?|session[-_ ]?|auth[-_ ]?)?(?:token|key|secret|password|authorization|credential)s?)(?:\\*["'])?\s*[:=\uFF1A\uFF1D]\s*/gi;
409
+ const NAME_BLANK_RANGES = [
410
+ [0x00a0, 0x00a0],
411
+ [0x1680, 0x1680],
412
+ [0x2000, 0x200a],
413
+ [0x202f, 0x202f],
414
+ [0x205f, 0x205f],
415
+ [0x3000, 0x3000],
416
+ [0x115f, 0x1160],
417
+ [0x3164, 0x3164],
418
+ [0xffa0, 0xffa0],
419
+ [0x2800, 0x2800],
420
+ ];
421
+ const isNameBlank = (cp) => NAME_BLANK_RANGES.some(([a, b]) => cp >= a && cp <= b);
422
+ const isFoldedBlank = (c) => isNameBlank(c) && isRegexSpace(c);
423
+ const isUnicodeBlank = (c) => (c > 0x7f && isRegexSpace(c) && c !== 0xfeff) || isNameBlank(c);
424
+ const SECRET_SCHEME_WORD = /(?:\b|(?<=\\[A-Za-z"']))(bearer|basic)[\s:=\uFF1A\uFF1D\u115F\u1160\u3164\uFFA0\u2800]+/gi;
425
+ const SECRET_LABELLED_WORD = /(?:\b|(?<=_)|(?<=\\[A-Za-z"']))((?:access[-_ ]?|refresh[-_ ]?|id[-_ ]?|client[-_ ]?|api[-_ ]?|x[-_]api[-_ ]?|session[-_ ]?|auth[-_ ]?)?(?:token|key|secret|password|authorization|credential)s?)(?:\\*["'])?[\s\u115F\u1160\u3164\uFFA0\u2800]*[:=\uFF1A\uFF1D][\s\u115F\u1160\u3164\uFFA0\u2800]*/gi;
368
426
  const DIAGNOSTIC_VALUE_WORDS = new Set([
369
427
  'missing', 'required', 'invalid', 'expired', 'unauthorized', 'unauthenticated', 'authentication',
370
428
  'authorization', 'credential', 'credentials', 'unsupported', 'malformed', 'rejected', 'absent',
@@ -566,6 +624,18 @@ function labelStartsAt(text, at) {
566
624
  return re.test(text);
567
625
  }
568
626
  function credentialWordStartsAt(text, at) {
627
+ if (at >= text.length)
628
+ return false;
629
+ if (credentialWordStartsExactlyAt(text, at))
630
+ return true;
631
+ let k = at;
632
+ while (k < text.length && text.charCodeAt(k) === 0x5c)
633
+ k++;
634
+ if (text[k] === '"' || text[k] === "'")
635
+ k++;
636
+ return k > at && credentialWordStartsExactlyAt(text, k);
637
+ }
638
+ function credentialWordStartsExactlyAt(text, at) {
569
639
  if (at >= text.length)
570
640
  return false;
571
641
  if (labelStartsAt(text, at))
@@ -730,7 +800,7 @@ function trailingLabelStart(text, from, end) {
730
800
  }
731
801
  const withoutSeparators = (s) => (s.includes(VIEW_SEPARATOR) ? s.split(VIEW_SEPARATOR).join('') : s);
732
802
  const isHorizontalSpace = (ch) => ch === ' ' || ch === '\t' || ch === '\u00a0' || ch === VIEW_SEPARATOR;
733
- function labelEdits(text, re) {
803
+ function labelEdits(text, re, literalBlanks = false) {
734
804
  const edits = [];
735
805
  const schemePass = re === SECRET_SCHEME_WORD;
736
806
  const idx = new InnerWordIndex(text);
@@ -751,6 +821,12 @@ function labelEdits(text, re) {
751
821
  re.lastIndex = v.end;
752
822
  continue;
753
823
  }
824
+ if (literalBlanks) {
825
+ if (literalBlankLeadJudgedAsBlank(text, v, label, schemePass)) {
826
+ re.lastIndex = v.end;
827
+ continue;
828
+ }
829
+ }
754
830
  const rawValue = text.slice(v.start, v.end);
755
831
  const value = withoutSeparators(rawValue);
756
832
  const quoted = v.start > 0 && (text[v.start - 1] === '"' || text[v.start - 1] === "'");
@@ -800,6 +876,34 @@ function labelEdits(text, re) {
800
876
  }
801
877
  return edits;
802
878
  }
879
+ function literalBlankLeadJudgedAsBlank(text, v, label, schemePass) {
880
+ let restStart;
881
+ if (v.start > 0 && text.charCodeAt(v.start - 1) === 0x5c && nameBlankTokenAt(text, v.start - 1) !== -1)
882
+ restStart = v.start + 5;
883
+ else if (nameBlankTokenAt(text, v.start) !== -1)
884
+ restStart = v.start + 6;
885
+ else if (eatenBlankTokenTailAt(text, v.start))
886
+ restStart = v.start + 5;
887
+ else
888
+ return false;
889
+ const rest = withoutSeparators(text.slice(restStart, v.end));
890
+ const bare = rest.replace(/[.,;:!?)\]}'"]+$/, '');
891
+ if (bare === '')
892
+ return true;
893
+ if (credentialWordStartsAt(' ' + text.slice(restStart, Math.min(text.length, v.end + 64)), 1))
894
+ return true;
895
+ if (schemePass && SCHEME_FOLLOWER_WORDS.has(bare.toLowerCase()))
896
+ return true;
897
+ if (!schemePass && isNamedNotValue(label, rest))
898
+ return true;
899
+ if (!schemePass && /authorizations?$/i.test(label) && isAuthScheme(bare))
900
+ return true;
901
+ return !isAddressShaped(rest) && isDiagnosticValue(rest);
902
+ }
903
+ function eatenBlankTokenTailAt(text, at) {
904
+ const hit = /^u([0-9A-Fa-f]{4})/.exec(text.slice(at, at + 5));
905
+ return hit !== null && isNameBlank(Number.parseInt(hit[1] ?? '', 16));
906
+ }
803
907
  function isAuthParamAt(text, j, scheme) {
804
908
  const p = /^[A-Za-z][A-Za-z0-9_-]*([ \t\ufeff]*)=([ \t\ufeff]*)("|[^\s=,"]+)([ \t\ufeff]*,)?/.exec(text.slice(j, j + 512));
805
909
  if (p === null)
@@ -991,49 +1095,75 @@ function proseLiteralEdits(text) {
991
1095
  }
992
1096
  return edits;
993
1097
  }
994
- function washWordNets(target) {
1098
+ function washWordNets(target, literalBlanks = false) {
995
1099
  target.apply(pemEdits(target.text));
996
- target.apply(labelEdits(target.text, SECRET_SCHEME_WORD));
997
- target.apply(labelEdits(target.text, SECRET_LABELLED_WORD));
1100
+ target.apply(labelEdits(target.text, SECRET_SCHEME_WORD, literalBlanks));
1101
+ target.apply(labelEdits(target.text, SECRET_LABELLED_WORD, literalBlanks));
998
1102
  target.apply(authParamEdits(target.text));
999
1103
  target.apply(proseLiteralEdits(target.text));
1000
1104
  }
1001
- function runCredentialNets(target, urls, words) {
1105
+ function runCredentialNets(target, urls, words, literalBlanks = false) {
1002
1106
  if (urls)
1003
1107
  washUrlNets(target);
1004
1108
  if (words)
1005
- washWordNets(target);
1109
+ washWordNets(target, literalBlanks);
1006
1110
  }
1007
- const NEEDS_VIEW = /[\u0000-\u0008\u000E-\u001F\u007F-\u009F]|\p{Cf}|\\u/u;
1111
+ const NEEDS_VIEW = /[\u0000-\u0008\u000E-\u001F\u007F-\u009F\uD800-\uDFFF]|\p{Cf}|\\u/u;
1008
1112
  const FORMAT_CHAR = /^\p{Cf}$/u;
1009
1113
  const isStrippableControl = (c) => (c <= 0x1f && (c < 0x09 || c > 0x0d)) || (c >= 0x7f && c <= 0x9f);
1010
- function csiEnd(text, from) {
1011
- let j = from;
1012
- while (j < text.length && text.charCodeAt(j) >= 0x30 && text.charCodeAt(j) <= 0x3f)
1013
- j++;
1014
- while (j < text.length && text.charCodeAt(j) >= 0x20 && text.charCodeAt(j) <= 0x2f)
1015
- j++;
1016
- const fin = text.charCodeAt(j);
1017
- return fin >= 0x40 && fin <= 0x7e ? j + 1 : -1;
1018
- }
1019
- const MAX_CONTROL_STRING = 4096;
1020
- class ControlStringEnds {
1114
+ class SequenceReader {
1021
1115
  text;
1116
+ atomicBlanks;
1117
+ foldBlanks;
1022
1118
  terminator;
1023
1119
  lineBreak;
1024
1120
  termFrom = -1;
1025
1121
  termHit = null;
1026
1122
  breakFrom = -1;
1027
1123
  breakAt = -1;
1028
- constructor(text) {
1124
+ constructor(text, atomicBlanks, foldBlanks = false) {
1029
1125
  this.text = text;
1030
- this.terminator = /[\u0007\u009c]|\u001b\\|\\[uU](?:0007|009[cC]|001[bB]\\)/g;
1126
+ this.atomicBlanks = atomicBlanks;
1127
+ this.foldBlanks = foldBlanks;
1128
+ this.terminator = /[\u0007\u009c]|\u001b\\|\\u(?:0007|009[cC]|001[bB]\\)/g;
1031
1129
  this.lineBreak = /[\n\r]/g;
1032
1130
  }
1033
- find(from) {
1131
+ atomicTokenAt(at) {
1132
+ if (this.text.charCodeAt(at) !== 0x5c)
1133
+ return false;
1134
+ if (escapeTokenAt(this.text, at) !== null)
1135
+ return true;
1136
+ if (this.atomicBlanks === 'none')
1137
+ return false;
1138
+ const cp = nameBlankTokenAt(this.text, at);
1139
+ return this.atomicBlanks === 'all' ? cp !== -1 : isFoldedBlank(cp);
1140
+ }
1141
+ intermediateAt(at) {
1142
+ const c = this.text.charCodeAt(at);
1143
+ if (c >= 0x20 && c <= 0x2f)
1144
+ return 1;
1145
+ if (!this.foldBlanks)
1146
+ return 0;
1147
+ if (isFoldedBlank(c))
1148
+ return 1;
1149
+ return c === 0x5c && isFoldedBlank(nameBlankTokenAt(this.text, at)) ? 6 : 0;
1150
+ }
1151
+ interruptsAt(at) {
1152
+ if (at >= this.text.length)
1153
+ return false;
1154
+ const c = this.text.charCodeAt(at);
1155
+ if (isStrippableControl(c))
1156
+ return true;
1157
+ if (c === 0x5c)
1158
+ return escapeTokenAt(this.text, at) !== null;
1159
+ if (c < 0xad)
1160
+ return false;
1161
+ const cp = this.text.codePointAt(at) ?? c;
1162
+ return (cp >= 0xd800 && cp <= 0xdfff) || FORMAT_CHAR.test(String.fromCodePoint(cp));
1163
+ }
1164
+ findControlStringEnd(from) {
1034
1165
  if (this.termFrom === -1 || from < this.termFrom || (this.termHit !== null && from > this.termHit.index)) {
1035
- this.terminator.lastIndex = from;
1036
- this.termHit = this.terminator.exec(this.text);
1166
+ this.termHit = this.nextTerminator(from);
1037
1167
  this.termFrom = from;
1038
1168
  }
1039
1169
  if (this.breakFrom === -1 || from < this.breakFrom || (this.breakAt !== -1 && from > this.breakAt)) {
@@ -1049,34 +1179,57 @@ class ControlStringEnds {
1049
1179
  return null;
1050
1180
  return { contentEnd: hit.index, end: hit.index + hit[0].length };
1051
1181
  }
1182
+ nextTerminator(from) {
1183
+ this.terminator.lastIndex = from;
1184
+ for (let hit = this.terminator.exec(this.text); hit !== null; hit = this.terminator.exec(this.text)) {
1185
+ const last = hit.index + hit[0].length - 1;
1186
+ if (this.text.charCodeAt(last) !== 0x5c || !this.atomicTokenAt(last))
1187
+ return hit;
1188
+ this.terminator.lastIndex = last;
1189
+ }
1190
+ return null;
1191
+ }
1192
+ }
1193
+ function csiEnd(text, from, seq) {
1194
+ let j = from;
1195
+ while (j < text.length && text.charCodeAt(j) >= 0x30 && text.charCodeAt(j) <= 0x3f)
1196
+ j++;
1197
+ for (let w = seq.intermediateAt(j); w > 0 && j < text.length; w = seq.intermediateAt(j))
1198
+ j += w;
1199
+ if (seq.interruptsAt(j))
1200
+ return j;
1201
+ const fin = text.charCodeAt(j);
1202
+ if (fin === 0x5c && seq.atomicTokenAt(j))
1203
+ return j;
1204
+ return fin > 0x40 && fin <= 0x7e ? j + 1 : j;
1052
1205
  }
1053
- function controlString(from, ends) {
1054
- const s = ends.find(from);
1206
+ const MAX_CONTROL_STRING = 4096;
1207
+ function controlString(from, seq) {
1208
+ const s = seq.findControlStringEnd(from);
1055
1209
  return s === null ? { end: from } : { end: s.end, content: [from, s.contentEnd] };
1056
1210
  }
1057
- function escapeSequenceAfterEsc(text, at, ends) {
1211
+ function escapeSequenceAfterEsc(text, at, seq) {
1058
1212
  const n = text.charCodeAt(at);
1059
- if (n === 0x5b) {
1060
- const e = csiEnd(text, at + 1);
1061
- return e === -1 ? null : { end: e };
1062
- }
1213
+ if (n === 0x5b)
1214
+ return { end: csiEnd(text, at + 1, seq) };
1063
1215
  if (n === 0x5d || n === 0x50 || n === 0x58 || n === 0x5e || n === 0x5f)
1064
- return controlString(at + 1, ends);
1065
- if (n === 0x3a || n === 0x3d || (n >= 0x60 && n <= 0x7e))
1216
+ return controlString(at + 1, seq);
1217
+ if (n === 0x3a || n === 0x3d || n === 0x40 || (n >= 0x60 && n <= 0x7e))
1066
1218
  return null;
1067
1219
  let j = at;
1068
- while (j < text.length && text.charCodeAt(j) >= 0x20 && text.charCodeAt(j) <= 0x2f)
1069
- j++;
1220
+ for (let w = seq.intermediateAt(j); w > 0 && j < text.length; w = seq.intermediateAt(j))
1221
+ j += w;
1070
1222
  const f = text.charCodeAt(j);
1071
- return f >= 0x30 && f <= 0x7e ? { end: j + 1 } : null;
1072
- }
1073
- function c1SequenceAfter(text, c1, at, ends) {
1074
- if (c1 === 0x9b) {
1075
- const e = csiEnd(text, at);
1076
- return e === -1 ? null : { end: e };
1077
- }
1223
+ const complete = !seq.interruptsAt(j) && !(f === 0x5c && seq.atomicTokenAt(j)) && f >= 0x30 && f <= 0x7e && f !== 0x40;
1224
+ if (complete)
1225
+ return { end: j + 1 };
1226
+ return j > at ? { end: j } : null;
1227
+ }
1228
+ function c1SequenceAfter(text, c1, at, seq) {
1229
+ if (c1 === 0x9b)
1230
+ return { end: csiEnd(text, at, seq) };
1078
1231
  if (c1 === 0x9d || c1 === 0x90 || c1 === 0x98 || c1 === 0x9e || c1 === 0x9f)
1079
- return controlString(at, ends);
1232
+ return controlString(at, seq);
1080
1233
  return null;
1081
1234
  }
1082
1235
  function isFaceMarkedCodePoint(cp) {
@@ -1097,15 +1250,23 @@ function escapeTokenAt(text, at) {
1097
1250
  const cp = Number.parseInt(hit[1] ?? '', 16);
1098
1251
  return cp <= 0x10ffff && isFaceMarkedCodePoint(cp) ? { end: at + hit[0].length, cp } : null;
1099
1252
  }
1100
- function buildCredentialView(text, separated, wordLetters = false) {
1101
- if (!NEEDS_VIEW.test(text))
1253
+ function nameBlankTokenAt(text, at) {
1254
+ const hit = /^\\u([0-9A-Fa-f]{4})/.exec(text.slice(at, at + 6));
1255
+ if (hit === null)
1256
+ return -1;
1257
+ const cp = Number.parseInt(hit[1] ?? '', 16);
1258
+ return isNameBlank(cp) ? cp : -1;
1259
+ }
1260
+ function buildCredentialView(text, separated, wordLetters = false, blankTokens = 'off') {
1261
+ const folding = blankTokens === 'fold' || blankTokens === 'foldlit';
1262
+ if (!NEEDS_VIEW.test(text) && !(folding && hasRawFoldedBlank(text)))
1102
1263
  return null;
1103
1264
  const chars = [];
1104
1265
  const starts = [];
1105
1266
  const ends = [];
1106
1267
  let changed = false;
1107
1268
  let splitAny = false;
1108
- const stringEnds = new ControlStringEnds(text);
1269
+ const seq = new SequenceReader(text, blankTokens === 'blank' || blankTokens === 'fold' ? 'all' : blankTokens === 'foldlit' ? 'folded' : 'none', folding);
1109
1270
  const keep = (ch, s, e) => {
1110
1271
  chars.push(ch);
1111
1272
  starts.push(s);
@@ -1118,22 +1279,35 @@ function buildCredentialView(text, separated, wordLetters = false) {
1118
1279
  };
1119
1280
  const dropUnit = (s, unit) => {
1120
1281
  const last = unit.end - 1;
1121
- if (wordLetters && unit.content === undefined && last > s && credentialWordBeginsWith(text, last)) {
1282
+ if ((blankTokens === 'literal' || blankTokens === 'foldlit') && last > s && text.charCodeAt(last) === 0x5c && nameBlankTokenAt(text, last) !== -1) {
1283
+ drop(s, last);
1284
+ keep(text.charAt(last + 1), last, last + 6);
1285
+ for (let u = 2; u < 6; u++)
1286
+ keep(text.charAt(last + u), last + 6, last + 6);
1287
+ return last + 6;
1288
+ }
1289
+ if (wordLetters && unit.content === undefined && last > s && (credentialWordBeginsWith(text, last) || finalIsStructural(text, last))) {
1122
1290
  drop(s, last);
1123
1291
  keep(text.charAt(last), last, unit.end);
1124
1292
  splitAny = true;
1125
- return;
1293
+ return unit.end;
1126
1294
  }
1127
1295
  drop(s, unit.end);
1296
+ return unit.end;
1128
1297
  };
1129
1298
  let i = 0;
1130
1299
  while (i < text.length) {
1131
1300
  const c = text.charCodeAt(i);
1301
+ if (folding && isFoldedBlank(c)) {
1302
+ changed = true;
1303
+ keep(' ', i, i + 1);
1304
+ i++;
1305
+ continue;
1306
+ }
1132
1307
  if (c === 0x1b || c === 0x9b || c === 0x9d || c === 0x90 || c === 0x98 || c === 0x9e || c === 0x9f) {
1133
- const unit = c === 0x1b ? escapeSequenceAfterEsc(text, i + 1, stringEnds) : c1SequenceAfter(text, c, i + 1, stringEnds);
1308
+ const unit = c === 0x1b ? escapeSequenceAfterEsc(text, i + 1, seq) : c1SequenceAfter(text, c, i + 1, seq);
1134
1309
  if (unit !== null) {
1135
- dropUnit(i, unit);
1136
- i = unit.end;
1310
+ i = dropUnit(i, unit);
1137
1311
  continue;
1138
1312
  }
1139
1313
  drop(i, i + 1);
@@ -1144,19 +1318,37 @@ function buildCredentialView(text, separated, wordLetters = false) {
1144
1318
  const tok = escapeTokenAt(text, i);
1145
1319
  if (tok !== null) {
1146
1320
  const unit = tok.cp === 0x1b
1147
- ? escapeSequenceAfterEsc(text, tok.end, stringEnds)
1321
+ ? escapeSequenceAfterEsc(text, tok.end, seq)
1148
1322
  : tok.cp >= 0x90 && tok.cp <= 0x9f
1149
- ? c1SequenceAfter(text, tok.cp, tok.end, stringEnds)
1323
+ ? c1SequenceAfter(text, tok.cp, tok.end, seq)
1150
1324
  : null;
1151
1325
  if (unit !== null) {
1152
- dropUnit(i, unit);
1153
- i = unit.end;
1326
+ i = dropUnit(i, unit);
1154
1327
  continue;
1155
1328
  }
1156
1329
  drop(i, tok.end, ' ');
1157
1330
  i = tok.end;
1158
1331
  continue;
1159
1332
  }
1333
+ if (blankTokens !== 'off') {
1334
+ const cp = nameBlankTokenAt(text, i);
1335
+ if (cp !== -1) {
1336
+ changed = true;
1337
+ if (blankTokens === 'blank')
1338
+ keep(String.fromCharCode(cp), i, i + 6);
1339
+ else if (blankTokens === 'fold')
1340
+ keep(isFoldedBlank(cp) ? ' ' : String.fromCharCode(cp), i, i + 6);
1341
+ else if (blankTokens === 'foldlit' && isFoldedBlank(cp))
1342
+ keep(' ', i, i + 6);
1343
+ else {
1344
+ keep(text.charAt(i), i, i + 6);
1345
+ for (let u = 1; u < 6; u++)
1346
+ keep(text.charAt(i + u), i + 6, i + 6);
1347
+ }
1348
+ i += 6;
1349
+ continue;
1350
+ }
1351
+ }
1160
1352
  }
1161
1353
  if (isStrippableControl(c)) {
1162
1354
  drop(i, i + 1);
@@ -1166,7 +1358,7 @@ function buildCredentialView(text, separated, wordLetters = false) {
1166
1358
  if (c >= 0xad) {
1167
1359
  const cp = text.codePointAt(i) ?? c;
1168
1360
  const width = cp > 0xffff ? 2 : 1;
1169
- if (FORMAT_CHAR.test(String.fromCodePoint(cp))) {
1361
+ if (FORMAT_CHAR.test(String.fromCodePoint(cp)) || (cp >= 0xd800 && cp <= 0xdfff)) {
1170
1362
  drop(i, i + width);
1171
1363
  i += width;
1172
1364
  continue;
@@ -1183,7 +1375,13 @@ function buildCredentialView(text, separated, wordLetters = false) {
1183
1375
  return null;
1184
1376
  return { view: chars.join(''), starts: Int32Array.from(starts), ends: Int32Array.from(ends) };
1185
1377
  }
1186
- const isProbeFiller = (c) => c === 0x5c || c === 0x22 || c === 0x27 || isRegexSpace(c);
1378
+ function finalIsStructural(text, at) {
1379
+ if (text.charCodeAt(at) !== 0x5c)
1380
+ return false;
1381
+ const n = text.charCodeAt(at + 1);
1382
+ return n === 0x22 || n === 0x27;
1383
+ }
1384
+ const isProbeFiller = (c) => c === 0x5c || c === 0x22 || c === 0x27 || isRegexSpace(c) || isNameBlank(c);
1187
1385
  function credentialWordBeginsWith(text, at) {
1188
1386
  const c = text.charCodeAt(at);
1189
1387
  const letter = (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
@@ -1193,6 +1391,13 @@ function credentialWordBeginsWith(text, at) {
1193
1391
  const nextOk = (n >= 0x41 && n <= 0x5a) || (n >= 0x61 && n <= 0x7a) || (!letter && (n === 0x22 || n === 0x27));
1194
1392
  if (!nextOk)
1195
1393
  return false;
1394
+ if (letter) {
1395
+ let e = at;
1396
+ while (e < text.length && isKeyWordChar(text.charCodeAt(e)))
1397
+ e++;
1398
+ if (SECRET_LITERAL_RE.test(text.slice(at, e + 1)))
1399
+ return true;
1400
+ }
1196
1401
  let end = Math.min(text.length, at + 64);
1197
1402
  while (end < text.length && isProbeFiller(text.charCodeAt(end - 1)))
1198
1403
  end++;
@@ -1201,8 +1406,21 @@ function credentialWordBeginsWith(text, at) {
1201
1406
  return credentialWordStartsAt(probe, 1);
1202
1407
  return credentialWordStartsAt(probe, 2) || credentialWordStartsAt(probe, 3);
1203
1408
  }
1204
- function rebuildFromView(original, view, target) {
1409
+ function rebuildFromView(original, view, target, trace = false) {
1205
1410
  const out = [];
1411
+ const origin = trace ? [] : null;
1412
+ const fromOriginal = (a, b) => {
1413
+ out.push(original.slice(a, b));
1414
+ if (origin !== null)
1415
+ for (let x = a; x < b; x++)
1416
+ origin.push(x);
1417
+ };
1418
+ const inserted = (s) => {
1419
+ out.push(s);
1420
+ if (origin !== null)
1421
+ for (let x = 0; x < s.length; x++)
1422
+ origin.push(-1);
1423
+ };
1206
1424
  const viewLength = view.view.length;
1207
1425
  let cursor = 0;
1208
1426
  let k = 0;
@@ -1212,11 +1430,11 @@ function rebuildFromView(original, view, target) {
1212
1430
  if (s >= 0) {
1213
1431
  const end = view.ends[s] ?? cursor;
1214
1432
  if ((view.starts[s] ?? 0) >= cursor) {
1215
- out.push(original.slice(cursor, end));
1433
+ fromOriginal(cursor, end);
1216
1434
  cursor = end;
1217
1435
  }
1218
1436
  else
1219
- out.push(text.charAt(k));
1437
+ inserted(text.charAt(k));
1220
1438
  k++;
1221
1439
  continue;
1222
1440
  }
@@ -1232,38 +1450,144 @@ function rebuildFromView(original, view, target) {
1232
1450
  const from = a < viewLength ? (view.starts[a] ?? original.length) : original.length;
1233
1451
  const to = b > a ? (view.ends[b - 1] ?? from) : from;
1234
1452
  if (from > cursor)
1235
- out.push(original.slice(cursor, from));
1236
- out.push(text.slice(k, j));
1453
+ fromOriginal(cursor, from);
1454
+ inserted(text.slice(k, j));
1237
1455
  cursor = Math.max(cursor, to);
1238
1456
  k = j;
1239
1457
  }
1240
- out.push(original.slice(cursor));
1241
- return out.join('');
1458
+ fromOriginal(cursor, original.length);
1459
+ const rebuilt = out.join('');
1460
+ return origin === null ? { text: rebuilt } : { text: rebuilt, origin: Int32Array.from(origin) };
1242
1461
  }
1243
- function washThroughView(original, view, urls, words) {
1462
+ function washThroughView(original, view, urls, words, trace = false, literalBlanks = false) {
1244
1463
  const target = new TrackedTarget(view.view, view.view.length);
1245
- runCredentialNets(target, urls, words);
1246
- return target.text === view.view ? original : rebuildFromView(original, view, target);
1247
- }
1248
- function washCredentialNetsOnce(text, urls, words, nested) {
1249
- const contentsWashed = nested ? text : washControlStringContents(text, urls, words);
1250
- const lettered = buildCredentialView(contentsWashed, true, true);
1251
- const pre = lettered === null ? contentsWashed : washThroughView(contentsWashed, lettered, urls, words);
1252
- const separated = buildCredentialView(pre, true);
1253
- if (separated === null) {
1254
- const target = new PlainTarget(pre);
1255
- runCredentialNets(target, urls, words);
1256
- return target.text;
1464
+ runCredentialNets(target, urls, words, literalBlanks);
1465
+ return target.text === view.view ? { text: original } : rebuildFromView(original, view, target, trace);
1466
+ }
1467
+ function washCredentialNetsOnce(text, urls, words, nested, blankTokens, trace) {
1468
+ const contents = nested ? { text } : washControlStringContents(text, urls, words, blankTokens, trace);
1469
+ if (blankTokens === 'off')
1470
+ return washThroughViews(contents, urls, words, 'off', trace);
1471
+ const tokens = hasNameBlankToken(contents.text);
1472
+ const readings = [];
1473
+ if (tokens)
1474
+ readings.push('literal');
1475
+ const rawFolded = blankTokens === 'fold' && hasRawFoldedBlank(contents.text);
1476
+ if (blankTokens === 'fold' && (tokens || rawFolded))
1477
+ readings.push('fold');
1478
+ if (blankTokens === 'fold' && tokens && hasNameBlankToken(contents.text, false) && (rawFolded || hasNameBlankToken(contents.text, true)))
1479
+ readings.push('foldlit');
1480
+ readings.push('blank');
1481
+ if (readings.length === 1)
1482
+ return washThroughViews(contents, urls, words, 'blank', trace);
1483
+ const base = { text: contents.text };
1484
+ const union = unionMaskedRanges(base.text, readings.map((r) => washThroughViews(base, urls, words, r, true)));
1485
+ return traced(union.text, composeOrigin(contents.origin, union.origin));
1486
+ }
1487
+ function unionMaskedRanges(text, results) {
1488
+ const ranges = [];
1489
+ for (const r of results) {
1490
+ if (r.text === text || r.origin === undefined)
1491
+ continue;
1492
+ const origin = r.origin;
1493
+ let lastKept = -1;
1494
+ let k = 0;
1495
+ while (k < origin.length) {
1496
+ if ((origin[k] ?? -1) >= 0) {
1497
+ lastKept = origin[k] ?? lastKept;
1498
+ k++;
1499
+ continue;
1500
+ }
1501
+ let j = k;
1502
+ while (j < origin.length && (origin[j] ?? -1) < 0)
1503
+ j++;
1504
+ const next = j < origin.length ? (origin[j] ?? text.length) : text.length;
1505
+ ranges.push({ start: lastKept + 1, end: next, text: r.text.slice(k, j) });
1506
+ k = j;
1507
+ }
1508
+ }
1509
+ if (ranges.length === 0)
1510
+ return { text };
1511
+ ranges.sort((a, b) => (a.start !== b.start ? a.start - b.start : b.end - a.end));
1512
+ const merged = [];
1513
+ for (const m of ranges) {
1514
+ const last = merged[merged.length - 1];
1515
+ if (last !== undefined && (m.start < last.end || (m.start === last.start && m.end === last.end))) {
1516
+ if (m.end > last.end) {
1517
+ if (m.end - m.start > last.end - last.start)
1518
+ last.text = m.text;
1519
+ last.end = m.end;
1520
+ }
1521
+ continue;
1522
+ }
1523
+ if (last !== undefined && m.start === m.end && m.start > last.start && m.start < last.end)
1524
+ continue;
1525
+ merged.push({ ...m });
1526
+ }
1527
+ const parts = [];
1528
+ const origin = [];
1529
+ let cursor = 0;
1530
+ for (const m of merged) {
1531
+ parts.push(text.slice(cursor, m.start), m.text);
1532
+ for (let x = cursor; x < m.start; x++)
1533
+ origin.push(x);
1534
+ for (let x = 0; x < m.text.length; x++)
1535
+ origin.push(-1);
1536
+ cursor = m.end;
1257
1537
  }
1258
- const afterSeparated = washThroughView(pre, separated, urls, words);
1259
- const joined = buildCredentialView(afterSeparated, false);
1260
- return joined === null ? afterSeparated : washThroughView(afterSeparated, joined, urls, words);
1538
+ parts.push(text.slice(cursor));
1539
+ for (let x = cursor; x < text.length; x++)
1540
+ origin.push(x);
1541
+ return { text: parts.join(''), origin: Int32Array.from(origin) };
1261
1542
  }
1262
- function washControlStringContents(text, urls, words) {
1543
+ function hasRawFoldedBlank(text) {
1544
+ for (let k = 0; k < text.length; k++)
1545
+ if (isFoldedBlank(text.charCodeAt(k)))
1546
+ return true;
1547
+ return false;
1548
+ }
1549
+ function hasNameBlankToken(text, folded) {
1550
+ if (!text.includes('\\u'))
1551
+ return false;
1552
+ for (const hit of text.matchAll(/\\u([0-9A-Fa-f]{4})/g)) {
1553
+ const cp = Number.parseInt(hit[1] ?? '', 16);
1554
+ if (isNameBlank(cp) && (folded === undefined || isFoldedBlank(cp) === folded))
1555
+ return true;
1556
+ }
1557
+ return false;
1558
+ }
1559
+ function washThroughViews(input, urls, words, blankRead, trace) {
1560
+ const start = input.text;
1561
+ const literal = blankRead === 'literal' || blankRead === 'foldlit';
1562
+ const lettered = buildCredentialView(start, true, true, blankRead);
1563
+ const letteredWashed = lettered === null ? { text: start } : washThroughView(start, lettered, urls, words, trace, literal);
1564
+ const pre = letteredWashed.text;
1565
+ let origin = composeOrigin(input.origin, letteredWashed.origin);
1566
+ const separated = buildCredentialView(pre, true, false, blankRead);
1567
+ if (separated === null) {
1568
+ const target = new PlainTarget(pre, trace);
1569
+ runCredentialNets(target, urls, words, literal);
1570
+ return traced(target.text, composeOrigin(origin, target.origin));
1571
+ }
1572
+ const afterSeparated = washThroughView(pre, separated, urls, words, trace, literal);
1573
+ origin = composeOrigin(origin, afterSeparated.origin);
1574
+ const joined = buildCredentialView(afterSeparated.text, false, false, blankRead);
1575
+ if (joined === null)
1576
+ return traced(afterSeparated.text, origin);
1577
+ const afterJoined = washThroughView(afterSeparated.text, joined, urls, words, trace, literal);
1578
+ return traced(afterJoined.text, composeOrigin(origin, afterJoined.origin));
1579
+ }
1580
+ function washControlStringContents(text, urls, words, blankTokens, trace) {
1263
1581
  if (!/[\u001b\u0090\u0098\u009d-\u009f]|\\u/.test(text))
1264
- return text;
1265
- const ends = new ControlStringEnds(text);
1582
+ return { text };
1583
+ const ends = new SequenceReader(text, 'none');
1266
1584
  const parts = [];
1585
+ const origin = trace ? [] : null;
1586
+ const keptUpTo = (a, b) => {
1587
+ if (origin !== null)
1588
+ for (let x = a; x < b; x++)
1589
+ origin.push(x);
1590
+ };
1267
1591
  let cursor = 0;
1268
1592
  let i = 0;
1269
1593
  while (i < text.length) {
@@ -1288,32 +1612,45 @@ function washControlStringContents(text, urls, words) {
1288
1612
  if (unit.content !== undefined) {
1289
1613
  const [a, b] = unit.content;
1290
1614
  const inner = text.slice(a, b);
1291
- const washed = washCredentialNets(inner, urls, words, true);
1292
- if (washed !== inner) {
1293
- parts.push(text.slice(cursor, a), washed);
1615
+ const washed = washNets(inner, urls, words, true, blankTokens, trace);
1616
+ if (washed.text !== inner) {
1617
+ parts.push(text.slice(cursor, a), washed.text);
1618
+ keptUpTo(cursor, a);
1619
+ if (origin !== null) {
1620
+ const w = washed.origin;
1621
+ for (let x = 0; x < washed.text.length; x++) {
1622
+ const s = w === undefined ? x : (w[x] ?? -1);
1623
+ origin.push(s < 0 ? -1 : a + s);
1624
+ }
1625
+ }
1294
1626
  cursor = b;
1295
1627
  }
1296
1628
  }
1297
1629
  i = Math.max(unit.end, i + 1);
1298
1630
  }
1299
1631
  if (cursor === 0)
1300
- return text;
1632
+ return { text };
1301
1633
  parts.push(text.slice(cursor));
1302
- return parts.join('');
1634
+ keptUpTo(cursor, text.length);
1635
+ const joined = parts.join('');
1636
+ return origin === null ? { text: joined } : { text: joined, origin: Int32Array.from(origin) };
1303
1637
  }
1304
1638
  const MAX_SETTLE_ROUNDS = 4;
1305
- function washCredentialNets(text, urls, words, nested = false) {
1639
+ function washNets(text, urls, words, nested, blankTokens, trace) {
1306
1640
  if (!urls && !words)
1307
- return text;
1308
- let current = text;
1641
+ return { text };
1642
+ let current = { text };
1309
1643
  for (let round = 0; round < MAX_SETTLE_ROUNDS; round++) {
1310
- const next = washCredentialNetsOnce(current, urls, words, nested);
1311
- if (next === current)
1644
+ const next = washCredentialNetsOnce(current.text, urls, words, nested, blankTokens, trace);
1645
+ if (next.text === current.text)
1312
1646
  return current;
1313
- current = next;
1647
+ current = traced(next.text, composeOrigin(current.origin, next.origin));
1314
1648
  }
1315
1649
  return current;
1316
1650
  }
1651
+ function washCredentialNets(text, urls, words, nested = false, blankTokens = 'off') {
1652
+ return washNets(text, urls, words, nested, blankTokens, false).text;
1653
+ }
1317
1654
  export function washCredentials(text) {
1318
1655
  if (typeof text !== 'string')
1319
1656
  return text;
@@ -1324,7 +1661,111 @@ const C0_KEEP_LAYOUT = '\\u0000-\\u0008\\u000B-\\u001F';
1324
1661
  const DEL_C1 = '\\u007F-\\u009F';
1325
1662
  const LONE_SURROGATE = '\\uD800-\\uDFFF';
1326
1663
  const LINE_SEPARATORS = '\\u2028\\u2029';
1327
- const FORMAT_CLASS = '(?![\\u200C\\u200D])\\p{Cf}';
1664
+ const NAME_BLANKS = NAME_BLANK_RANGES.map(([a, b]) => (a === b ? classEscape(a) : `${classEscape(a)}-${classEscape(b)}`)).join('');
1665
+ function classEscape(cp) {
1666
+ return `\\u${cp.toString(16).toUpperCase().padStart(4, '0')}`;
1667
+ }
1668
+ function compilesAsUnicodeRegExp(source) {
1669
+ try {
1670
+ return new RegExp(source, 'u').unicode;
1671
+ }
1672
+ catch {
1673
+ return false;
1674
+ }
1675
+ }
1676
+ const JOINING_SCRIPT_CANDIDATES = [
1677
+ 'Arab', 'Syrc', 'Mand', 'Mani', 'Phlp', 'Sogd', 'Ougr', 'Chrs', 'Mong', 'Phag', 'Nkoo', 'Adlm', 'Rohg',
1678
+ 'Deva', 'Beng', 'Guru', 'Gujr', 'Orya', 'Taml', 'Telu', 'Knda', 'Mlym', 'Sinh',
1679
+ 'Brah', 'Kthi', 'Shrd', 'Sidd', 'Tirh', 'Gran', 'Newa', 'Takr', 'Modi', 'Mahj', 'Khoj', 'Sind', 'Dogr', 'Nand',
1680
+ 'Gonm', 'Gong', 'Diak', 'Bhks', 'Zanb', 'Soyo', 'Ahom', 'Cakm', 'Sylo', 'Saur', 'Mtei', 'Limb', 'Lepc', 'Kawi',
1681
+ 'Mymr', 'Khmr', 'Thai', 'Laoo', 'Tibt', 'Bali', 'Java', 'Sund', 'Lana', 'Tavt', 'Talu', 'Tale', 'Cham', 'Batk',
1682
+ 'Bugi', 'Maka', 'Rjng', 'Kali', 'Tglg', 'Hano', 'Buhd', 'Tagb',
1683
+ ];
1684
+ const JOINING_SCRIPTS = JOINING_SCRIPT_CANDIDATES.filter((sc) => compilesAsUnicodeRegExp(`\\p{scx=${sc}}`));
1685
+ function codePointTest(source) {
1686
+ if (!compilesAsUnicodeRegExp(source))
1687
+ return () => false;
1688
+ const re = new RegExp(`^${source}$`, 'u');
1689
+ return (cp) => re.test(String.fromCodePoint(cp));
1690
+ }
1691
+ const isClusterExtender = codePointTest('[\\p{M}\\p{Emoji_Modifier}]');
1692
+ const isPictograph = codePointTest('\\p{Extended_Pictographic}');
1693
+ const isEmojiPresentation = codePointTest('\\p{Emoji_Presentation}');
1694
+ const isEmojiModifier = codePointTest('\\p{Emoji_Modifier}');
1695
+ const isJoiningLetter = JOINING_SCRIPTS.length === 0
1696
+ ? () => false
1697
+ : codePointTest('(?=\\p{L})(?!(?=\\p{Lm})[\\p{sc=Zyyy}\\p{sc=Zinh}])[' + JOINING_SCRIPTS.map((sc) => '\\p{scx=' + sc + '}').join('') + ']');
1698
+ const VIRAMA_CODE_POINTS = [
1699
+ 0x094d, 0x09cd, 0x0a4d, 0x0acd, 0x0b4d, 0x0bcd, 0x0c4d, 0x0ccd, 0x0d3b, 0x0d3c, 0x0d4d, 0x0dca,
1700
+ 0x0e3a, 0x0eba, 0x0f84, 0x1039, 0x103a, 0x1714, 0x1715, 0x1734, 0x17d2, 0x1a60, 0x1b44, 0x1baa, 0x1bab, 0x1bf2, 0x1bf3,
1701
+ 0xa806, 0xa82c, 0xa8c4, 0xa953, 0xa9c0, 0xaaf6, 0xabed,
1702
+ 0x11046, 0x11070, 0x1107f, 0x110b9, 0x11133, 0x11134, 0x111c0, 0x11235, 0x112ea, 0x1134d, 0x11442, 0x114c2, 0x115bf,
1703
+ 0x1163f, 0x116b6, 0x1172b, 0x11839, 0x1193d, 0x1193e, 0x119e0, 0x11a34, 0x11a47, 0x11a99, 0x11c3f, 0x11d44, 0x11d45,
1704
+ 0x11d97, 0x11f41, 0x11f42,
1705
+ ];
1706
+ const VIRAMAS = new Set(VIRAMA_CODE_POINTS);
1707
+ function joinerKept(text, at, zwj) {
1708
+ let p = at;
1709
+ let left = -1;
1710
+ let base = -1;
1711
+ let afterBase = -1;
1712
+ let visited = -1;
1713
+ while (p > 0) {
1714
+ let q = p - 1;
1715
+ const unit = text.charCodeAt(q);
1716
+ if (unit >= 0xdc00 && unit <= 0xdfff && q > 0) {
1717
+ const hi = text.charCodeAt(q - 1);
1718
+ if (hi >= 0xd800 && hi <= 0xdbff)
1719
+ q -= 1;
1720
+ }
1721
+ const cp = text.codePointAt(q) ?? unit;
1722
+ if (left === -1)
1723
+ left = cp;
1724
+ p = q;
1725
+ if (!isClusterExtender(cp)) {
1726
+ base = cp;
1727
+ afterBase = visited;
1728
+ break;
1729
+ }
1730
+ visited = cp;
1731
+ }
1732
+ if (base === -1)
1733
+ return false;
1734
+ let r = at + 1;
1735
+ let next = -1;
1736
+ let rightBase = -1;
1737
+ while (r < text.length) {
1738
+ const cp = text.codePointAt(r) ?? 0;
1739
+ if (next === -1)
1740
+ next = cp;
1741
+ r += cp > 0xffff ? 2 : 1;
1742
+ if (!isClusterExtender(cp)) {
1743
+ rightBase = cp;
1744
+ break;
1745
+ }
1746
+ }
1747
+ if (zwj && next !== -1) {
1748
+ const nextEnd = at + 1 + (next > 0xffff ? 2 : 1);
1749
+ const afterNext = nextEnd < text.length ? (text.codePointAt(nextEnd) ?? -1) : -1;
1750
+ if (emojiPresented(base, afterBase) && emojiPresented(next, afterNext))
1751
+ return true;
1752
+ }
1753
+ if (!isJoiningLetter(base))
1754
+ return false;
1755
+ if (rightBase !== -1 && isJoiningLetter(rightBase))
1756
+ return true;
1757
+ return zwj && VIRAMAS.has(left);
1758
+ }
1759
+ function emojiPresented(cp, after) {
1760
+ return isPictograph(cp) && (isEmojiPresentation(cp) || after === 0xfe0f || (after !== -1 && isEmojiModifier(after)));
1761
+ }
1762
+ function faceHitKept(ch, at, text) {
1763
+ if (ch.length > 2)
1764
+ return true;
1765
+ const c = ch.charCodeAt(0);
1766
+ return ch.length === 1 && (c === 0x200d || c === 0x200c) && joinerKept(text, at, c === 0x200d);
1767
+ }
1768
+ const FORMAT_CLASS = '\\p{Cf}';
1328
1769
  const RGI_TAG_FLAG = '\\u{1F3F4}\\u{E0067}\\u{E0062}(?:\\u{E0065}\\u{E006E}\\u{E0067}|\\u{E0073}\\u{E0063}\\u{E0074}|\\u{E0077}\\u{E006C}\\u{E0073})\\u{E007F}';
1329
1770
  const BIDI_REORDER = '\\u061C\\u200E\\u200F\\u2028\\u2029\\u202A-\\u202E\\u2066-\\u2069';
1330
1771
  function unsafeClass(members, withFormatClass) {
@@ -1357,27 +1798,173 @@ function markOf(ch, mark) {
1357
1798
  const hex = code.toString(16).toUpperCase();
1358
1799
  return code > 0xffff ? `\\u{${hex}}` : `\\u${hex.padStart(4, '0')}`;
1359
1800
  }
1360
- const foldWhitespace = (s) => s.replace(/\s+/g, ' ').trim();
1801
+ const FOLD_RUN = /\s+/g;
1802
+ const FOLD_RUN_KEEPING_BLANKS = new RegExp(`[^\\S${NAME_BLANKS}]+`, 'g');
1803
+ const EDGE_SPACE = /^ | $/g;
1804
+ const foldWhitespace = (s, keepBlanks = false) => keepBlanks ? s.replace(FOLD_RUN_KEEPING_BLANKS, ' ').replace(EDGE_SPACE, '') : s.replace(FOLD_RUN, ' ').trim();
1361
1805
  export function applyDisplayFace(text, face) {
1362
- const markAll = (s) => face.unsafe === null ? s : s.replace(face.unsafe, ch => (ch.length > 2 ? ch : markOf(ch, face.mark)));
1806
+ const markAll = (s) => face.unsafe === null ? s : s.replace(face.unsafe, (ch, at, whole) => (faceHitKept(ch, at, whole) ? ch : markOf(ch, face.mark)));
1807
+ const keepBlanks = face.keepBlanks === true;
1363
1808
  if (face.mark === 'space') {
1364
1809
  const marked = markAll(text);
1365
- return face.fold ? foldWhitespace(marked) : marked;
1810
+ return face.fold ? foldWhitespace(marked, keepBlanks) : marked;
1811
+ }
1812
+ return markAll(face.fold ? foldWhitespace(text, keepBlanks) : text);
1813
+ }
1814
+ function markTracked(t, re, mark, ledger) {
1815
+ const parts = [];
1816
+ const ids = [];
1817
+ const keep = (a, b) => {
1818
+ parts.push(t.text.slice(a, b));
1819
+ for (let k = a; k < b; k++)
1820
+ ids.push(t.ids[k] ?? -1);
1821
+ };
1822
+ let cursor = 0;
1823
+ for (const hit of t.text.matchAll(re)) {
1824
+ const at = hit.index ?? 0;
1825
+ const ch = hit[0];
1826
+ keep(cursor, at);
1827
+ if (faceHitKept(ch, at, t.text))
1828
+ keep(at, at + ch.length);
1829
+ else {
1830
+ const form = markOf(ch, mark);
1831
+ const id = ledger.cps.length;
1832
+ ledger.cps.push(ch.codePointAt(0) ?? 0);
1833
+ ledger.lens.push(form.length);
1834
+ parts.push(form);
1835
+ for (let x = 0; x < form.length; x++)
1836
+ ids.push(id);
1837
+ }
1838
+ cursor = at + ch.length;
1366
1839
  }
1367
- return markAll(face.fold ? foldWhitespace(text) : text);
1840
+ if (cursor === 0)
1841
+ return t;
1842
+ keep(cursor, t.text.length);
1843
+ return { text: parts.join(''), ids: Int32Array.from(ids) };
1368
1844
  }
1369
- const PUBLIC_UNSAFE_BY_NETS = Object.freeze(Array.from({ length: 8 }, (_, i) => {
1370
- const controls = (i & 4) !== 0;
1371
- const bidi = (i & 2) !== 0;
1372
- const keep = (i & 1) !== 0;
1373
- const members = `${controls ? `${keep ? C0_KEEP_LAYOUT : C0_ALL}${DEL_C1}${LONE_SURROGATE}` : ''}${bidi ? LINE_SEPARATORS : ''}`;
1845
+ function foldTracked(t, keepBlanks) {
1846
+ const parts = [];
1847
+ const ids = [];
1848
+ let cursor = 0;
1849
+ for (const run of t.text.matchAll(keepBlanks ? FOLD_RUN_KEEPING_BLANKS : FOLD_RUN)) {
1850
+ const at = run.index ?? 0;
1851
+ parts.push(t.text.slice(cursor, at));
1852
+ for (let k = cursor; k < at; k++)
1853
+ ids.push(t.ids[k] ?? -1);
1854
+ let id = -1;
1855
+ for (let k = at; k < at + run[0].length && id < 0; k++)
1856
+ id = t.ids[k] ?? -1;
1857
+ parts.push(' ');
1858
+ ids.push(id);
1859
+ cursor = at + run[0].length;
1860
+ }
1861
+ parts.push(t.text.slice(cursor));
1862
+ for (let k = cursor; k < t.text.length; k++)
1863
+ ids.push(t.ids[k] ?? -1);
1864
+ const folded = parts.join('');
1865
+ let from = 0;
1866
+ let to = folded.length;
1867
+ if (folded.charCodeAt(0) === 0x20)
1868
+ from = 1;
1869
+ if (to > from && folded.charCodeAt(to - 1) === 0x20)
1870
+ to -= 1;
1871
+ return { text: folded.slice(from, to), ids: Int32Array.from(ids.slice(from, to)) };
1872
+ }
1873
+ function faceTracked(t, face, ledger) {
1874
+ const keepBlanks = face.keepBlanks === true;
1875
+ const unsafe = face.unsafe;
1876
+ const markAll = (x) => (unsafe === null ? x : markTracked(x, unsafe, face.mark, ledger));
1877
+ if (face.mark === 'space') {
1878
+ const marked = markAll(t);
1879
+ return face.fold ? foldTracked(marked, keepBlanks) : marked;
1880
+ }
1881
+ return markAll(face.fold ? foldTracked(t, keepBlanks) : t);
1882
+ }
1883
+ function carryIds(ids, origin) {
1884
+ if (origin === undefined)
1885
+ return ids;
1886
+ const out = new Int32Array(origin.length);
1887
+ for (let k = 0; k < origin.length; k++) {
1888
+ const s = origin[k] ?? -1;
1889
+ out[k] = s < 0 ? -1 : (ids[s] ?? -1);
1890
+ }
1891
+ return out;
1892
+ }
1893
+ function pairLiteralBackslashes(t) {
1894
+ if (!t.text.includes('\\'))
1895
+ return t;
1896
+ const parts = [];
1897
+ const ids = [];
1898
+ for (let k = 0; k < t.text.length; k++) {
1899
+ const ch = t.text.charAt(k);
1900
+ const id = t.ids[k] ?? -1;
1901
+ parts.push(ch);
1902
+ ids.push(id);
1903
+ if (ch === '\\' && id < 0) {
1904
+ parts.push('\\');
1905
+ ids.push(-1);
1906
+ }
1907
+ }
1908
+ return { text: parts.join(''), ids: Int32Array.from(ids) };
1909
+ }
1910
+ function capPaired(t, max) {
1911
+ if (typeof max !== 'number' || !Number.isFinite(max))
1912
+ return t;
1913
+ const limit = Math.max(0, Math.floor(max));
1914
+ let cost = 0;
1915
+ let cut = 0;
1916
+ while (cut < t.text.length) {
1917
+ const width = t.text.charCodeAt(cut) === 0x5c && (t.ids[cut] ?? -1) < 0 ? 2 : 1;
1918
+ if (cost + width > limit)
1919
+ break;
1920
+ cost += width;
1921
+ cut++;
1922
+ }
1923
+ if (cut === t.text.length)
1924
+ return t;
1925
+ const id = t.ids[cut] ?? -1;
1926
+ if (id >= 0)
1927
+ while (cut > 0 && t.ids[cut - 1] === id)
1928
+ cut--;
1929
+ if (cut > 0) {
1930
+ const code = t.text.charCodeAt(cut - 1);
1931
+ if (code >= 0xd800 && code <= 0xdbff)
1932
+ cut -= 1;
1933
+ }
1934
+ return { text: t.text.slice(0, cut), ids: t.ids.slice(0, cut) };
1935
+ }
1936
+ function collectMarks(t, ledger) {
1937
+ const out = [];
1938
+ let k = 0;
1939
+ while (k < t.ids.length) {
1940
+ const id = t.ids[k] ?? -1;
1941
+ if (id < 0) {
1942
+ k++;
1943
+ continue;
1944
+ }
1945
+ let j = k + 1;
1946
+ while (j < t.ids.length && t.ids[j] === id)
1947
+ j++;
1948
+ if (j - k === ledger.lens[id])
1949
+ out.push({ start: k, end: j, codePoint: ledger.cps[id] ?? 0 });
1950
+ k = j;
1951
+ }
1952
+ return out;
1953
+ }
1954
+ const PUBLIC_UNSAFE_BY_NETS = Object.freeze(Array.from({ length: 16 }, (_, i) => {
1955
+ const controls = (i & 8) !== 0;
1956
+ const bidi = (i & 4) !== 0;
1957
+ const keep = (i & 2) !== 0;
1958
+ const blanks = (i & 1) !== 0;
1959
+ const members = `${controls ? `${keep ? C0_KEEP_LAYOUT : C0_ALL}${DEL_C1}${LONE_SURROGATE}` : ''}${bidi ? LINE_SEPARATORS : ''}${blanks ? NAME_BLANKS : ''}`;
1374
1960
  return unsafeClass(members, bidi);
1375
1961
  }));
1376
1962
  function publicFace(opts) {
1377
1963
  const fold = opts.foldLines !== false;
1378
1964
  const keep = !fold && opts.keepLayout === true;
1379
- const index = (opts.controls !== false ? 4 : 0) + (opts.bidi !== false ? 2 : 0) + (keep ? 1 : 0);
1380
- return { unsafe: PUBLIC_UNSAFE_BY_NETS[index] ?? null, fold, mark: opts.mark ?? 'escape' };
1965
+ const blanks = opts.blanks === true;
1966
+ const index = (opts.controls !== false ? 8 : 0) + (opts.bidi !== false ? 4 : 0) + (keep ? 2 : 0) + (blanks ? 1 : 0);
1967
+ return { unsafe: PUBLIC_UNSAFE_BY_NETS[index] ?? null, fold, mark: opts.mark ?? 'escape', keepBlanks: blanks };
1381
1968
  }
1382
1969
  const ESCAPE_TOKEN_AT = /^\\u(?:[0-9A-F]{4}|\{[0-9A-F]{1,6}\})/;
1383
1970
  function capDisplay(text, max, mark) {
@@ -1405,33 +1992,72 @@ function capDisplay(text, max, mark) {
1405
1992
  return text.slice(0, cut);
1406
1993
  }
1407
1994
  const MAX_CAP_SETTLE_ROUNDS = 4;
1408
- export function displayUntrusted(text, opts = {}) {
1409
- const input = typeof text === 'string' ? text : String(text ?? '');
1410
- const o = opts ?? {};
1411
- const urls = o.credentialUrls !== false;
1412
- const words = o.credentialWords !== false;
1413
- const face = publicFace(o);
1995
+ function runDisplaySteps(start, steps) {
1414
1996
  const settle = (s) => {
1415
- let current = applyDisplayFace(washCredentialNets(s, urls, words), face);
1416
- if (current === s)
1997
+ let current = steps.paint(steps.wash(s));
1998
+ if (steps.text(current) === steps.text(s))
1417
1999
  return current;
1418
2000
  for (let round = 1; round < MAX_SETTLE_ROUNDS; round++) {
1419
- const next = applyDisplayFace(washCredentialNets(current, urls, words), face);
1420
- if (next === current)
2001
+ const next = steps.paint(steps.wash(current));
2002
+ if (steps.text(next) === steps.text(current))
1421
2003
  return current;
1422
2004
  current = next;
1423
2005
  }
1424
2006
  return current;
1425
2007
  };
1426
- const settled = settle(input);
1427
- let out = capDisplay(settled, o.max, face.mark);
1428
- if (out === settled)
2008
+ const settled = settle(start);
2009
+ let out = steps.cap(settled);
2010
+ if (steps.text(out) === steps.text(settled))
1429
2011
  return out;
1430
2012
  for (let round = 0; round < MAX_CAP_SETTLE_ROUNDS; round++) {
1431
- const again = capDisplay(settle(out), o.max, face.mark);
1432
- if (again === out)
2013
+ const again = steps.cap(settle(out));
2014
+ if (steps.text(again) === steps.text(out))
1433
2015
  break;
1434
2016
  out = again;
1435
2017
  }
1436
2018
  return out;
1437
2019
  }
2020
+ function renderUntrusted(text, opts, ledger) {
2021
+ const input = typeof text === 'string' ? text : String(text ?? '');
2022
+ const o = opts ?? {};
2023
+ const urls = o.credentialUrls !== false;
2024
+ const words = o.credentialWords !== false;
2025
+ const face = publicFace(o);
2026
+ const blankTokens = face.keepBlanks !== true ? 'off' : face.fold ? 'fold' : 'keep';
2027
+ const pairBackslashes = o.escapeBackslashes === true && face.mark !== 'dot' && face.mark !== 'space';
2028
+ const book = ledger ?? (pairBackslashes ? { cps: [], lens: [] } : null);
2029
+ if (book === null) {
2030
+ return runDisplaySteps(input, {
2031
+ wash: (s) => washCredentialNets(s, urls, words, false, blankTokens),
2032
+ paint: (s) => applyDisplayFace(s, face),
2033
+ cap: (s) => capDisplay(s, o.max, face.mark),
2034
+ text: (s) => s,
2035
+ });
2036
+ }
2037
+ const out = runDisplaySteps({ text: input, ids: new Int32Array(input.length).fill(-1) }, {
2038
+ wash: (t) => {
2039
+ const r = washNets(t.text, urls, words, false, blankTokens, true);
2040
+ return r.text === t.text ? t : { text: r.text, ids: carryIds(t.ids, r.origin) };
2041
+ },
2042
+ paint: (t) => faceTracked(t, face, book),
2043
+ cap: (t) => {
2044
+ if (pairBackslashes)
2045
+ return capPaired(t, o.max);
2046
+ const cut = capDisplay(t.text, o.max, face.mark);
2047
+ return cut === t.text ? t : { text: cut, ids: t.ids.slice(0, cut.length) };
2048
+ },
2049
+ text: (t) => t.text,
2050
+ });
2051
+ return pairBackslashes ? pairLiteralBackslashes(out) : out;
2052
+ }
2053
+ export function displayUntrusted(text, opts = {}) {
2054
+ const out = renderUntrusted(text, opts, null);
2055
+ return typeof out === 'string' ? out : out.text;
2056
+ }
2057
+ export function displayUntrustedMarks(text, opts = {}) {
2058
+ const ledger = { cps: [], lens: [] };
2059
+ const out = renderUntrusted(text, opts, ledger);
2060
+ if (typeof out === 'string')
2061
+ return { text: out, marks: [] };
2062
+ return { text: out.text, marks: collectMarks(out, ledger) };
2063
+ }