@holmes-lab/holmes-kit 0.1.18 → 0.2.0
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.
- package/CHANGELOG.md +18 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-context.d.ts +2 -0
- package/dist/holmes/cli/approve-context.js +180 -0
- package/dist/holmes/cli/approve-ref.d.ts +27 -0
- package/dist/holmes/cli/approve-ref.js +40 -0
- package/dist/holmes/cli/approve-watch.d.ts +29 -0
- package/dist/holmes/cli/approve-watch.js +94 -0
- package/dist/holmes/cli/approve.d.ts +50 -13
- package/dist/holmes/cli/approve.js +354 -38
- package/dist/holmes/cli/doctor.js +182 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +4 -0
- package/dist/holmes/cli/gitignore-merge.js +17 -1
- package/dist/holmes/cli/index.d.ts +23 -0
- package/dist/holmes/cli/index.js +487 -20
- package/dist/holmes/cli/init.js +14 -0
- package/dist/holmes/cli/screen-safe.d.ts +94 -0
- package/dist/holmes/cli/screen-safe.js +760 -0
- package/dist/holmes/governance/approval-queue.js +56 -4
- package/dist/holmes/governance/ledger-rechain.d.ts +25 -0
- package/dist/holmes/governance/ledger-rechain.js +95 -0
- package/dist/holmes/governance/provenance-chain.d.ts +33 -6
- package/dist/holmes/governance/provenance-chain.js +91 -16
- package/dist/holmes/governance/provenance-ledger.d.ts +7 -0
- package/dist/holmes/governance/provenance-ledger.js +10 -0
- package/dist/holmes/guardrail/risk-gate.d.ts +11 -1
- package/dist/holmes/guardrail/risk-gate.js +10 -0
- package/dist/holmes/mcp/elicit-approval.d.ts +67 -0
- package/dist/holmes/mcp/elicit-approval.js +79 -0
- package/dist/holmes/mcp/handlers.d.ts +7 -2
- package/dist/holmes/mcp/handlers.js +190 -24
- package/dist/holmes/mcp/server.js +26 -1
- package/dist/holmes/spec/id-collision.d.ts +39 -0
- package/dist/holmes/spec/id-collision.js +86 -0
- package/dist/holmes/spec/spec-store.js +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-262.1
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.KIND_COLS = exports.ROW_COLS = exports.ROW_MAX = void 0;
|
|
5
|
+
exports.isClipped = isClipped;
|
|
6
|
+
exports.displayWidth = displayWidth;
|
|
7
|
+
exports.flattenField = flattenField;
|
|
8
|
+
exports.rowLiteral = rowLiteral;
|
|
9
|
+
exports.rowField = rowField;
|
|
10
|
+
exports.wrapColumns = wrapColumns;
|
|
11
|
+
exports.stripControl = stripControl;
|
|
12
|
+
exports.clipCodePoints = clipCodePoints;
|
|
13
|
+
exports.shellQuote = shellQuote;
|
|
14
|
+
exports.safeRef = safeRef;
|
|
15
|
+
/**
|
|
16
|
+
* The decision screen shows AGENT-CONTROLLED text.
|
|
17
|
+
*
|
|
18
|
+
* A queue entry's `target` is whatever the agent was blocked on (the hook enqueues the command
|
|
19
|
+
* verbatim), and `why`/`question`/`reason` travel the same way. Round-1 measured the cost of raw
|
|
20
|
+
* interpolation: a crafted target forged list rows and hid the real ones behind an ANSI conceal, and
|
|
21
|
+
* the operator granted a `curl … | sh` they never saw. LINE STRUCTURE IS THE TEMPLATE'S, never the
|
|
22
|
+
* data's (the elicitation dialog's lesson, A-SPEC-263.1 §5b, on the other surface).
|
|
23
|
+
*
|
|
24
|
+
* Three properties this module must hold, each learned from a round that broke one of them:
|
|
25
|
+
* 1. NOTHING HIDDEN — no escape, control byte, or bidi mark reaches the terminal (round-1).
|
|
26
|
+
* 2. NOTHING VANISHES SILENTLY — a removal is marked, and the marker states its SIZE, because a
|
|
27
|
+
* removal whose size is invisible is still a concealment (round-2 made the sanitiser itself a
|
|
28
|
+
* concealment primitive; round-3 gave the marker a size).
|
|
29
|
+
* 3. NOTHING INFLATES — the output is bounded, or escape spam floods the very confirmation screen
|
|
30
|
+
* that exists so nothing is approved unseen (round-4: a max of 8000 could return 56,009 chars,
|
|
31
|
+
* ~467 terminal rows, scrolling the summaries away).
|
|
32
|
+
*
|
|
33
|
+
* A RUN of removable characters collapses to ONE marker carrying the run's length: that is what
|
|
34
|
+
* makes (2) and (3) compatible — 8000 escapes become `⟪8000자 제거⟫`, not 8000 markers.
|
|
35
|
+
*
|
|
36
|
+
* PURE. Control characters appear here as escapes only: a literal one in source is itself a thing
|
|
37
|
+
* the reader cannot see.
|
|
38
|
+
*/
|
|
39
|
+
// C0 (newlines handled per function), DEL, C1, NEL/LS/PS, bidi controls, zero-width and BOM.
|
|
40
|
+
// Round-5 completed this: U+061C ALM (the seventh Bidi_Control, strong RTL) survived while its six
|
|
41
|
+
// siblings were removed, and the whole Default_Ignorable smuggling set — soft hyphen, the Mongolian
|
|
42
|
+
// and Hangul fillers, U+2060-206F, the TAG block U+E0000-E007F — passed through invisibly. TAG
|
|
43
|
+
// characters inside a URL are the sharp case: the operator reads a benign address while the grant's
|
|
44
|
+
// scope pattern is the RAW target. Lone surrogates go too (they render as U+FFFD, the very glyph
|
|
45
|
+
// round-1's code-point clipping exists to prevent, arriving by another door).
|
|
46
|
+
const CONTROL_CLASS = '\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f'
|
|
47
|
+
+ '\\u00ad\\u034f\\u061c\\u115f\\u1160\\u17b4\\u17b5\\u180b-\\u180e'
|
|
48
|
+
+ '\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u206f'
|
|
49
|
+
+ '\\u2028\\u2029\\u0085\\u3164\\ud800-\\udfff\\ufff9-\\ufffb\\ufeff';
|
|
50
|
+
// The TAG block is astral, so it needs its own alternative rather than a BMP class entry.
|
|
51
|
+
const TAG_BLOCK = '\\u{E0000}-\\u{E007F}';
|
|
52
|
+
// ANSI escapes: CSI (colour, cursor, conceal) and the two-byte introducers — which already include
|
|
53
|
+
// `ESC ]` (OSC), `ESC \\` (ST), `ESC P` (DCS), `ESC ^`/`ESC _`. Round-4: a dedicated OSC alternative
|
|
54
|
+
// that consumed to end of line was honest but useless — an entire command vanished behind one
|
|
55
|
+
// marker, on the screen whose job is showing the subject. Removing only the INTRODUCER leaves the
|
|
56
|
+
// payload as ordinary, inert, VISIBLE text (the ESC is gone, so no terminal will act on it).
|
|
57
|
+
const ANSI_SRC = '\\u001b\\[[0-?]*[ -/]*[@-~]|\\u001b[@-Z\\\\-_]|\\u001b';
|
|
58
|
+
/** One or more removable things in a row — collapsed to a single marker (round-4). */
|
|
59
|
+
const REMOVABLE_RUN = new RegExp(`(?:${ANSI_SRC}|[${CONTROL_CLASS}]|[${TAG_BLOCK}])+`, 'gu');
|
|
60
|
+
/** Newline-preserving variant: a body's newlines are its content. */
|
|
61
|
+
const REMOVABLE_RUN_KEEP_NL = new RegExp(`(?:${ANSI_SRC}|[${CONTROL_CLASS}]|[${TAG_BLOCK}])+`, 'gu');
|
|
62
|
+
/**
|
|
63
|
+
* TAB is the one whitespace character that MOVES THE CURSOR BY AN AMOUNT THE DATA CHOOSES: the
|
|
64
|
+
* terminal advances to the next multiple-of-eight column, while every width function here — and the
|
|
65
|
+
* fold that depends on it — reads one. Round-7 measured the consequence: a spec body of tabs made
|
|
66
|
+
* `wrapColumns` believe a line was 47 columns, the terminal wrapped it anyway, and forged rows
|
|
67
|
+
* landed at column 0 of the `[v]` screen with ZERO escape bytes — defeating round-6's margin by the
|
|
68
|
+
* same alignment trick the margin was added to stop. It is expanded to a single space before any
|
|
69
|
+
* measurement: a substitution the operator can see, not a removal, and one column is one column.
|
|
70
|
+
*/
|
|
71
|
+
const TAB_TO_SPACE = /\t/gu;
|
|
72
|
+
/** One token at a time: a removable run, a whitespace run, or one visible code point. */
|
|
73
|
+
const TOKEN = new RegExp(`(?:${ANSI_SRC}|[${CONTROL_CLASS}]|[${TAG_BLOCK}])+|[\\r\\n\\t ]+|[\\s\\S]`, 'gu');
|
|
74
|
+
/**
|
|
75
|
+
* One terminal row's worth of SUBJECT, with the template's own cells already subtracted: an 80
|
|
76
|
+
* column row, minus the widest template prefix on this screen (`\u2713 \uc2b9\uc778 \u2014 `, nine columns)
|
|
77
|
+
* and the kind cell (16) and its space, leaves 52. Every line the decision surface prints is bounded by it, so a queue of any size stays a
|
|
78
|
+
* screen the operator can read top to bottom — the property the batch confirmation depends on
|
|
79
|
+
* (round-5: the field was bounded and the LINE was not, so a bounded field still wrapped).
|
|
80
|
+
*/
|
|
81
|
+
/** The terminal this screen promises to fit. The floor no terminal goes below. */
|
|
82
|
+
exports.ROW_MAX = 80;
|
|
83
|
+
exports.ROW_COLS = 50;
|
|
84
|
+
/**
|
|
85
|
+
* The `kind` cell. Round-7: at 16 the truncation NOTICE (`… (200자 잘림)`, fifteen columns) consumed
|
|
86
|
+
* the entire budget, so a non-ASCII kind rendered ZERO columns of the operator's text — the category
|
|
87
|
+
* of authority being granted, unreadable, in the list AND the item header AND the confirmation AND
|
|
88
|
+
* every decision echo. That is the round-5 lesson ("a bound met by showing nothing is the
|
|
89
|
+
* concealment this module exists to prevent") reached through a budget nobody re-checked. Twenty
|
|
90
|
+
* columns plus the short notice form below leaves real text on the line.
|
|
91
|
+
*/
|
|
92
|
+
exports.KIND_COLS = 20;
|
|
93
|
+
const MARK_OPEN = '\u27ea';
|
|
94
|
+
const MARK_CLOSE = '\u27eb';
|
|
95
|
+
/**
|
|
96
|
+
* Only the sanitiser may write a marker (round-4): ⟪ and ⟫ are ordinary printable characters, so a
|
|
97
|
+
* target could otherwise print `⟪7자 제거⟫` itself and make a real concealment read as routine noise
|
|
98
|
+
* — or claim a removal that never happened. The delimiters are neutralised on the way in.
|
|
99
|
+
*/
|
|
100
|
+
// Round-5: substituting `<`/`>` silently rewrote the subject into something SCARIER (a forged shell
|
|
101
|
+
// redirection). The direction was safe — it can only cause a spurious refusal — but this screen was
|
|
102
|
+
// rebuilt around "the grant is for what the human saw", so the swap uses look-alike brackets that
|
|
103
|
+
// are not shell metacharacters and cannot be mistaken for the sanitiser's own marker.
|
|
104
|
+
const neutraliseMarkers = (s) => s.split(MARK_OPEN).join('\u27e8').split(MARK_CLOSE).join('\u27e9');
|
|
105
|
+
const mark = (run) => `${MARK_OPEN}${[...run].length}자 제거${MARK_CLOSE}`;
|
|
106
|
+
/**
|
|
107
|
+
* How much was not shown. The long form reads better; on a narrow cell it would BE the cell, so a
|
|
108
|
+
* compact form is used there (round-7 measured fifteen columns of notice inside a sixteen-column
|
|
109
|
+
* budget — zero columns left for the operator's text). Eight columns instead of fifteen keeps the
|
|
110
|
+
* round-5 rule that half the budget reaches the subject. `isClipped` knows both shapes, so no caller
|
|
111
|
+
* ever has to parse a number to learn that something was hidden.
|
|
112
|
+
*/
|
|
113
|
+
const shortNotice = (max, hidden) => max < 30 ? `…(${hidden}자)` : `… (${hidden}자 잘림)`;
|
|
114
|
+
/**
|
|
115
|
+
* Did rendering this value into `max` columns hide any of it?
|
|
116
|
+
*
|
|
117
|
+
* Round-8: this used to be a REGEX OVER THE RENDERED TEXT, and the data can end with any string it
|
|
118
|
+
* likes — a target ending `… (5자 잘림)` was reported as clipped when nothing had been clipped, so
|
|
119
|
+
* `[A]` refused to batch it and told the operator, falsely, that it did not fit on a row. An agent
|
|
120
|
+
* could disable batch approval for a whole session by choosing its own suffix. The renderer knows
|
|
121
|
+
* the answer; nobody should have to parse it back out of the output.
|
|
122
|
+
*/
|
|
123
|
+
function isClipped(value, max = exports.ROW_COLS) {
|
|
124
|
+
return renderRow(String(value), max).hidden > 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How many terminal COLUMNS a string occupies. Round-5 measured why a code-point budget is not a
|
|
128
|
+
* screen budget: a 200-code-point line of markers rendered 302 columns — four wrapped rows at 80 —
|
|
129
|
+
* because `⟪`, `⟫` and every Hangul character in `자 제거` are East Asian WIDE (two columns each).
|
|
130
|
+
* The budget exists to bound SCREEN SPACE, so it must be counted in the unit the screen uses.
|
|
131
|
+
*/
|
|
132
|
+
function displayWidth(s) {
|
|
133
|
+
let w = 0;
|
|
134
|
+
for (const ch of s) {
|
|
135
|
+
const cp = ch.codePointAt(0);
|
|
136
|
+
// The fast path stops at U+00A1, the first code point that is not plainly one cell (round-6:
|
|
137
|
+
// it used to stop at U+0300 and swallowed the Ambiguous Latin-1 punctuation — `·`, the very
|
|
138
|
+
// separator this screen's own detail row is built from, measured 1 instead of 2).
|
|
139
|
+
if (cp < 0x00a1) {
|
|
140
|
+
w += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (inRanges(ZERO, cp))
|
|
144
|
+
continue; // stacks on the previous cell
|
|
145
|
+
w += inRanges(WIDE, cp) ? 2 : 1;
|
|
146
|
+
}
|
|
147
|
+
return w;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Column-width tables, GENERATED from the Unicode Character Database 16.0.0: WIDE holds
|
|
151
|
+
* every code point whose East_Asian_Width is W, F **or A**, ZERO holds categories Mn and Me. Flat
|
|
152
|
+
* [lo,hi,lo,hi,…] pairs, binary-searched.
|
|
153
|
+
*
|
|
154
|
+
* Round-6 replaced a hand-written list that named the blocks someone thought of and missed 8,685
|
|
155
|
+
* Wide code points — \u{1F680} U+1F680, U+2705, U+2B50, the whole U+1F650..U+1F8FF span, Tangut. A
|
|
156
|
+
* 52-column budget of rocket emoji measured 104 columns, so `[A]` batched items that could not be
|
|
157
|
+
* shown whole and the confirmation ran 43 rows on a 24-row terminal: round-4's flood reached with no
|
|
158
|
+
* escape byte at all. An enumeration written from memory is a defect with a delay on it.
|
|
159
|
+
*
|
|
160
|
+
* AMBIGUOUS counts as WIDE deliberately. A terminal renders those one cell wide by default and two
|
|
161
|
+
* in CJK mode, so no single number is right — but the two errors are not symmetric. Over-counting
|
|
162
|
+
* makes a line SHORTER than its budget; under-counting makes it overflow, and overflow is how this
|
|
163
|
+
* screen loses the evidence it exists to show. Much of this UI's own furniture is Ambiguous — `·`,
|
|
164
|
+
* `…`, `─`, `◆` — and this operator's terminal is a Korean one. (The removal marker's brackets
|
|
165
|
+
* `\u27ea`/`\u27eb` are Na, narrow: round-7 corrected this comment, which still claimed otherwise.)
|
|
166
|
+
*/
|
|
167
|
+
const WIDE = [
|
|
168
|
+
0x000a1, 0x000a1, 0x000a4, 0x000a4, 0x000a7, 0x000a8, 0x000aa, 0x000aa, 0x000ad, 0x000ae,
|
|
169
|
+
0x000b0, 0x000b4, 0x000b6, 0x000ba, 0x000bc, 0x000bf, 0x000c6, 0x000c6, 0x000d0, 0x000d0,
|
|
170
|
+
0x000d7, 0x000d8, 0x000de, 0x000e1, 0x000e6, 0x000e6, 0x000e8, 0x000ea, 0x000ec, 0x000ed,
|
|
171
|
+
0x000f0, 0x000f0, 0x000f2, 0x000f3, 0x000f7, 0x000fa, 0x000fc, 0x000fc, 0x000fe, 0x000fe,
|
|
172
|
+
0x00101, 0x00101, 0x00111, 0x00111, 0x00113, 0x00113, 0x0011b, 0x0011b, 0x00126, 0x00127,
|
|
173
|
+
0x0012b, 0x0012b, 0x00131, 0x00133, 0x00138, 0x00138, 0x0013f, 0x00142, 0x00144, 0x00144,
|
|
174
|
+
0x00148, 0x0014b, 0x0014d, 0x0014d, 0x00152, 0x00153, 0x00166, 0x00167, 0x0016b, 0x0016b,
|
|
175
|
+
0x001ce, 0x001ce, 0x001d0, 0x001d0, 0x001d2, 0x001d2, 0x001d4, 0x001d4, 0x001d6, 0x001d6,
|
|
176
|
+
0x001d8, 0x001d8, 0x001da, 0x001da, 0x001dc, 0x001dc, 0x00251, 0x00251, 0x00261, 0x00261,
|
|
177
|
+
0x002c4, 0x002c4, 0x002c7, 0x002c7, 0x002c9, 0x002cb, 0x002cd, 0x002cd, 0x002d0, 0x002d0,
|
|
178
|
+
0x002d8, 0x002db, 0x002dd, 0x002dd, 0x002df, 0x002df, 0x00300, 0x0036f, 0x00391, 0x003a1,
|
|
179
|
+
0x003a3, 0x003a9, 0x003b1, 0x003c1, 0x003c3, 0x003c9, 0x00401, 0x00401, 0x00410, 0x0044f,
|
|
180
|
+
0x00451, 0x00451, 0x01100, 0x0115f, 0x02010, 0x02010, 0x02013, 0x02016, 0x02018, 0x02019,
|
|
181
|
+
0x0201c, 0x0201d, 0x02020, 0x02022, 0x02024, 0x02027, 0x02030, 0x02030, 0x02032, 0x02033,
|
|
182
|
+
0x02035, 0x02035, 0x0203b, 0x0203b, 0x0203e, 0x0203e, 0x02074, 0x02074, 0x0207f, 0x0207f,
|
|
183
|
+
0x02081, 0x02084, 0x020ac, 0x020ac, 0x02103, 0x02103, 0x02105, 0x02105, 0x02109, 0x02109,
|
|
184
|
+
0x02113, 0x02113, 0x02116, 0x02116, 0x02121, 0x02122, 0x02126, 0x02126, 0x0212b, 0x0212b,
|
|
185
|
+
0x02153, 0x02154, 0x0215b, 0x0215e, 0x02160, 0x0216b, 0x02170, 0x02179, 0x02189, 0x02189,
|
|
186
|
+
0x02190, 0x02199, 0x021b8, 0x021b9, 0x021d2, 0x021d2, 0x021d4, 0x021d4, 0x021e7, 0x021e7,
|
|
187
|
+
0x02200, 0x02200, 0x02202, 0x02203, 0x02207, 0x02208, 0x0220b, 0x0220b, 0x0220f, 0x0220f,
|
|
188
|
+
0x02211, 0x02211, 0x02215, 0x02215, 0x0221a, 0x0221a, 0x0221d, 0x02220, 0x02223, 0x02223,
|
|
189
|
+
0x02225, 0x02225, 0x02227, 0x0222c, 0x0222e, 0x0222e, 0x02234, 0x02237, 0x0223c, 0x0223d,
|
|
190
|
+
0x02248, 0x02248, 0x0224c, 0x0224c, 0x02252, 0x02252, 0x02260, 0x02261, 0x02264, 0x02267,
|
|
191
|
+
0x0226a, 0x0226b, 0x0226e, 0x0226f, 0x02282, 0x02283, 0x02286, 0x02287, 0x02295, 0x02295,
|
|
192
|
+
0x02299, 0x02299, 0x022a5, 0x022a5, 0x022bf, 0x022bf, 0x02312, 0x02312, 0x0231a, 0x0231b,
|
|
193
|
+
0x02329, 0x0232a, 0x023e9, 0x023ec, 0x023f0, 0x023f0, 0x023f3, 0x023f3, 0x02460, 0x024e9,
|
|
194
|
+
0x024eb, 0x0254b, 0x02550, 0x02573, 0x02580, 0x0258f, 0x02592, 0x02595, 0x025a0, 0x025a1,
|
|
195
|
+
0x025a3, 0x025a9, 0x025b2, 0x025b3, 0x025b6, 0x025b7, 0x025bc, 0x025bd, 0x025c0, 0x025c1,
|
|
196
|
+
0x025c6, 0x025c8, 0x025cb, 0x025cb, 0x025ce, 0x025d1, 0x025e2, 0x025e5, 0x025ef, 0x025ef,
|
|
197
|
+
0x025fd, 0x025fe, 0x02605, 0x02606, 0x02609, 0x02609, 0x0260e, 0x0260f, 0x02614, 0x02615,
|
|
198
|
+
0x0261c, 0x0261c, 0x0261e, 0x0261e, 0x02630, 0x02637, 0x02640, 0x02640, 0x02642, 0x02642,
|
|
199
|
+
0x02648, 0x02653, 0x02660, 0x02661, 0x02663, 0x02665, 0x02667, 0x0266a, 0x0266c, 0x0266d,
|
|
200
|
+
0x0266f, 0x0266f, 0x0267f, 0x0267f, 0x0268a, 0x0268f, 0x02693, 0x02693, 0x0269e, 0x0269f,
|
|
201
|
+
0x026a1, 0x026a1, 0x026aa, 0x026ab, 0x026bd, 0x026bf, 0x026c4, 0x026e1, 0x026e3, 0x026e3,
|
|
202
|
+
0x026e8, 0x026ff, 0x02705, 0x02705, 0x0270a, 0x0270b, 0x02728, 0x02728, 0x0273d, 0x0273d,
|
|
203
|
+
0x0274c, 0x0274c, 0x0274e, 0x0274e, 0x02753, 0x02755, 0x02757, 0x02757, 0x02776, 0x0277f,
|
|
204
|
+
0x02795, 0x02797, 0x027b0, 0x027b0, 0x027bf, 0x027bf, 0x02b1b, 0x02b1c, 0x02b50, 0x02b50,
|
|
205
|
+
0x02b55, 0x02b59, 0x02e80, 0x02e99, 0x02e9b, 0x02ef3, 0x02f00, 0x02fd5, 0x02ff0, 0x0303e,
|
|
206
|
+
0x03041, 0x03096, 0x03099, 0x030ff, 0x03105, 0x0312f, 0x03131, 0x0318e, 0x03190, 0x031e5,
|
|
207
|
+
0x031ef, 0x0321e, 0x03220, 0x0a48c, 0x0a490, 0x0a4c6, 0x0a960, 0x0a97c, 0x0ac00, 0x0d7a3,
|
|
208
|
+
0x0e000, 0x0faff, 0x0fe00, 0x0fe19, 0x0fe30, 0x0fe52, 0x0fe54, 0x0fe66, 0x0fe68, 0x0fe6b,
|
|
209
|
+
0x0ff01, 0x0ff60, 0x0ffe0, 0x0ffe6, 0x0fffd, 0x0fffd, 0x16fe0, 0x16fe4, 0x16ff0, 0x16ff1,
|
|
210
|
+
0x17000, 0x187f7, 0x18800, 0x18cd5, 0x18cff, 0x18d08, 0x1aff0, 0x1aff3, 0x1aff5, 0x1affb,
|
|
211
|
+
0x1affd, 0x1affe, 0x1b000, 0x1b122, 0x1b132, 0x1b132, 0x1b150, 0x1b152, 0x1b155, 0x1b155,
|
|
212
|
+
0x1b164, 0x1b167, 0x1b170, 0x1b2fb, 0x1d300, 0x1d356, 0x1d360, 0x1d376, 0x1f004, 0x1f004,
|
|
213
|
+
0x1f0cf, 0x1f0cf, 0x1f100, 0x1f10a, 0x1f110, 0x1f12d, 0x1f130, 0x1f169, 0x1f170, 0x1f1ac,
|
|
214
|
+
0x1f200, 0x1f202, 0x1f210, 0x1f23b, 0x1f240, 0x1f248, 0x1f250, 0x1f251, 0x1f260, 0x1f265,
|
|
215
|
+
0x1f300, 0x1f320, 0x1f32d, 0x1f335, 0x1f337, 0x1f37c, 0x1f37e, 0x1f393, 0x1f3a0, 0x1f3ca,
|
|
216
|
+
0x1f3cf, 0x1f3d3, 0x1f3e0, 0x1f3f0, 0x1f3f4, 0x1f3f4, 0x1f3f8, 0x1f43e, 0x1f440, 0x1f440,
|
|
217
|
+
0x1f442, 0x1f4fc, 0x1f4ff, 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567, 0x1f57a, 0x1f57a,
|
|
218
|
+
0x1f595, 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f, 0x1f680, 0x1f6c5, 0x1f6cc, 0x1f6cc,
|
|
219
|
+
0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d7, 0x1f6dc, 0x1f6df, 0x1f6eb, 0x1f6ec, 0x1f6f4, 0x1f6fc,
|
|
220
|
+
0x1f7e0, 0x1f7eb, 0x1f7f0, 0x1f7f0, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947, 0x1f9ff,
|
|
221
|
+
0x1fa70, 0x1fa7c, 0x1fa80, 0x1fa89, 0x1fa8f, 0x1fac6, 0x1face, 0x1fadc, 0x1fadf, 0x1fae9,
|
|
222
|
+
0x1faf0, 0x1faf8, 0x20000, 0x2fffd, 0x30000, 0x3fffd, 0xe0100, 0xe01ef, 0xf0000, 0xffffd,
|
|
223
|
+
0x100000, 0x10fffd,
|
|
224
|
+
];
|
|
225
|
+
const ZERO = [
|
|
226
|
+
0x00300, 0x0036f, 0x00483, 0x00489, 0x00591, 0x005bd, 0x005bf, 0x005bf, 0x005c1, 0x005c2,
|
|
227
|
+
0x005c4, 0x005c5, 0x005c7, 0x005c7, 0x00610, 0x0061a, 0x0064b, 0x0065f, 0x00670, 0x00670,
|
|
228
|
+
0x006d6, 0x006dc, 0x006df, 0x006e4, 0x006e7, 0x006e8, 0x006ea, 0x006ed, 0x00711, 0x00711,
|
|
229
|
+
0x00730, 0x0074a, 0x007a6, 0x007b0, 0x007eb, 0x007f3, 0x007fd, 0x007fd, 0x00816, 0x00819,
|
|
230
|
+
0x0081b, 0x00823, 0x00825, 0x00827, 0x00829, 0x0082d, 0x00859, 0x0085b, 0x00897, 0x0089f,
|
|
231
|
+
0x008ca, 0x008e1, 0x008e3, 0x00902, 0x0093a, 0x0093a, 0x0093c, 0x0093c, 0x00941, 0x00948,
|
|
232
|
+
0x0094d, 0x0094d, 0x00951, 0x00957, 0x00962, 0x00963, 0x00981, 0x00981, 0x009bc, 0x009bc,
|
|
233
|
+
0x009c1, 0x009c4, 0x009cd, 0x009cd, 0x009e2, 0x009e3, 0x009fe, 0x009fe, 0x00a01, 0x00a02,
|
|
234
|
+
0x00a3c, 0x00a3c, 0x00a41, 0x00a42, 0x00a47, 0x00a48, 0x00a4b, 0x00a4d, 0x00a51, 0x00a51,
|
|
235
|
+
0x00a70, 0x00a71, 0x00a75, 0x00a75, 0x00a81, 0x00a82, 0x00abc, 0x00abc, 0x00ac1, 0x00ac5,
|
|
236
|
+
0x00ac7, 0x00ac8, 0x00acd, 0x00acd, 0x00ae2, 0x00ae3, 0x00afa, 0x00aff, 0x00b01, 0x00b01,
|
|
237
|
+
0x00b3c, 0x00b3c, 0x00b3f, 0x00b3f, 0x00b41, 0x00b44, 0x00b4d, 0x00b4d, 0x00b55, 0x00b56,
|
|
238
|
+
0x00b62, 0x00b63, 0x00b82, 0x00b82, 0x00bc0, 0x00bc0, 0x00bcd, 0x00bcd, 0x00c00, 0x00c00,
|
|
239
|
+
0x00c04, 0x00c04, 0x00c3c, 0x00c3c, 0x00c3e, 0x00c40, 0x00c46, 0x00c48, 0x00c4a, 0x00c4d,
|
|
240
|
+
0x00c55, 0x00c56, 0x00c62, 0x00c63, 0x00c81, 0x00c81, 0x00cbc, 0x00cbc, 0x00cbf, 0x00cbf,
|
|
241
|
+
0x00cc6, 0x00cc6, 0x00ccc, 0x00ccd, 0x00ce2, 0x00ce3, 0x00d00, 0x00d01, 0x00d3b, 0x00d3c,
|
|
242
|
+
0x00d41, 0x00d44, 0x00d4d, 0x00d4d, 0x00d62, 0x00d63, 0x00d81, 0x00d81, 0x00dca, 0x00dca,
|
|
243
|
+
0x00dd2, 0x00dd4, 0x00dd6, 0x00dd6, 0x00e31, 0x00e31, 0x00e34, 0x00e3a, 0x00e47, 0x00e4e,
|
|
244
|
+
0x00eb1, 0x00eb1, 0x00eb4, 0x00ebc, 0x00ec8, 0x00ece, 0x00f18, 0x00f19, 0x00f35, 0x00f35,
|
|
245
|
+
0x00f37, 0x00f37, 0x00f39, 0x00f39, 0x00f71, 0x00f7e, 0x00f80, 0x00f84, 0x00f86, 0x00f87,
|
|
246
|
+
0x00f8d, 0x00f97, 0x00f99, 0x00fbc, 0x00fc6, 0x00fc6, 0x0102d, 0x01030, 0x01032, 0x01037,
|
|
247
|
+
0x01039, 0x0103a, 0x0103d, 0x0103e, 0x01058, 0x01059, 0x0105e, 0x01060, 0x01071, 0x01074,
|
|
248
|
+
0x01082, 0x01082, 0x01085, 0x01086, 0x0108d, 0x0108d, 0x0109d, 0x0109d, 0x0135d, 0x0135f,
|
|
249
|
+
0x01712, 0x01714, 0x01732, 0x01733, 0x01752, 0x01753, 0x01772, 0x01773, 0x017b4, 0x017b5,
|
|
250
|
+
0x017b7, 0x017bd, 0x017c6, 0x017c6, 0x017c9, 0x017d3, 0x017dd, 0x017dd, 0x0180b, 0x0180d,
|
|
251
|
+
0x0180f, 0x0180f, 0x01885, 0x01886, 0x018a9, 0x018a9, 0x01920, 0x01922, 0x01927, 0x01928,
|
|
252
|
+
0x01932, 0x01932, 0x01939, 0x0193b, 0x01a17, 0x01a18, 0x01a1b, 0x01a1b, 0x01a56, 0x01a56,
|
|
253
|
+
0x01a58, 0x01a5e, 0x01a60, 0x01a60, 0x01a62, 0x01a62, 0x01a65, 0x01a6c, 0x01a73, 0x01a7c,
|
|
254
|
+
0x01a7f, 0x01a7f, 0x01ab0, 0x01ace, 0x01b00, 0x01b03, 0x01b34, 0x01b34, 0x01b36, 0x01b3a,
|
|
255
|
+
0x01b3c, 0x01b3c, 0x01b42, 0x01b42, 0x01b6b, 0x01b73, 0x01b80, 0x01b81, 0x01ba2, 0x01ba5,
|
|
256
|
+
0x01ba8, 0x01ba9, 0x01bab, 0x01bad, 0x01be6, 0x01be6, 0x01be8, 0x01be9, 0x01bed, 0x01bed,
|
|
257
|
+
0x01bef, 0x01bf1, 0x01c2c, 0x01c33, 0x01c36, 0x01c37, 0x01cd0, 0x01cd2, 0x01cd4, 0x01ce0,
|
|
258
|
+
0x01ce2, 0x01ce8, 0x01ced, 0x01ced, 0x01cf4, 0x01cf4, 0x01cf8, 0x01cf9, 0x01dc0, 0x01dff,
|
|
259
|
+
0x020d0, 0x020f0, 0x02cef, 0x02cf1, 0x02d7f, 0x02d7f, 0x02de0, 0x02dff, 0x0302a, 0x0302d,
|
|
260
|
+
0x03099, 0x0309a, 0x0a66f, 0x0a672, 0x0a674, 0x0a67d, 0x0a69e, 0x0a69f, 0x0a6f0, 0x0a6f1,
|
|
261
|
+
0x0a802, 0x0a802, 0x0a806, 0x0a806, 0x0a80b, 0x0a80b, 0x0a825, 0x0a826, 0x0a82c, 0x0a82c,
|
|
262
|
+
0x0a8c4, 0x0a8c5, 0x0a8e0, 0x0a8f1, 0x0a8ff, 0x0a8ff, 0x0a926, 0x0a92d, 0x0a947, 0x0a951,
|
|
263
|
+
0x0a980, 0x0a982, 0x0a9b3, 0x0a9b3, 0x0a9b6, 0x0a9b9, 0x0a9bc, 0x0a9bd, 0x0a9e5, 0x0a9e5,
|
|
264
|
+
0x0aa29, 0x0aa2e, 0x0aa31, 0x0aa32, 0x0aa35, 0x0aa36, 0x0aa43, 0x0aa43, 0x0aa4c, 0x0aa4c,
|
|
265
|
+
0x0aa7c, 0x0aa7c, 0x0aab0, 0x0aab0, 0x0aab2, 0x0aab4, 0x0aab7, 0x0aab8, 0x0aabe, 0x0aabf,
|
|
266
|
+
0x0aac1, 0x0aac1, 0x0aaec, 0x0aaed, 0x0aaf6, 0x0aaf6, 0x0abe5, 0x0abe5, 0x0abe8, 0x0abe8,
|
|
267
|
+
0x0abed, 0x0abed, 0x0fb1e, 0x0fb1e, 0x0fe00, 0x0fe0f, 0x0fe20, 0x0fe2f, 0x101fd, 0x101fd,
|
|
268
|
+
0x102e0, 0x102e0, 0x10376, 0x1037a, 0x10a01, 0x10a03, 0x10a05, 0x10a06, 0x10a0c, 0x10a0f,
|
|
269
|
+
0x10a38, 0x10a3a, 0x10a3f, 0x10a3f, 0x10ae5, 0x10ae6, 0x10d24, 0x10d27, 0x10d69, 0x10d6d,
|
|
270
|
+
0x10eab, 0x10eac, 0x10efc, 0x10eff, 0x10f46, 0x10f50, 0x10f82, 0x10f85, 0x11001, 0x11001,
|
|
271
|
+
0x11038, 0x11046, 0x11070, 0x11070, 0x11073, 0x11074, 0x1107f, 0x11081, 0x110b3, 0x110b6,
|
|
272
|
+
0x110b9, 0x110ba, 0x110c2, 0x110c2, 0x11100, 0x11102, 0x11127, 0x1112b, 0x1112d, 0x11134,
|
|
273
|
+
0x11173, 0x11173, 0x11180, 0x11181, 0x111b6, 0x111be, 0x111c9, 0x111cc, 0x111cf, 0x111cf,
|
|
274
|
+
0x1122f, 0x11231, 0x11234, 0x11234, 0x11236, 0x11237, 0x1123e, 0x1123e, 0x11241, 0x11241,
|
|
275
|
+
0x112df, 0x112df, 0x112e3, 0x112ea, 0x11300, 0x11301, 0x1133b, 0x1133c, 0x11340, 0x11340,
|
|
276
|
+
0x11366, 0x1136c, 0x11370, 0x11374, 0x113bb, 0x113c0, 0x113ce, 0x113ce, 0x113d0, 0x113d0,
|
|
277
|
+
0x113d2, 0x113d2, 0x113e1, 0x113e2, 0x11438, 0x1143f, 0x11442, 0x11444, 0x11446, 0x11446,
|
|
278
|
+
0x1145e, 0x1145e, 0x114b3, 0x114b8, 0x114ba, 0x114ba, 0x114bf, 0x114c0, 0x114c2, 0x114c3,
|
|
279
|
+
0x115b2, 0x115b5, 0x115bc, 0x115bd, 0x115bf, 0x115c0, 0x115dc, 0x115dd, 0x11633, 0x1163a,
|
|
280
|
+
0x1163d, 0x1163d, 0x1163f, 0x11640, 0x116ab, 0x116ab, 0x116ad, 0x116ad, 0x116b0, 0x116b5,
|
|
281
|
+
0x116b7, 0x116b7, 0x1171d, 0x1171d, 0x1171f, 0x1171f, 0x11722, 0x11725, 0x11727, 0x1172b,
|
|
282
|
+
0x1182f, 0x11837, 0x11839, 0x1183a, 0x1193b, 0x1193c, 0x1193e, 0x1193e, 0x11943, 0x11943,
|
|
283
|
+
0x119d4, 0x119d7, 0x119da, 0x119db, 0x119e0, 0x119e0, 0x11a01, 0x11a0a, 0x11a33, 0x11a38,
|
|
284
|
+
0x11a3b, 0x11a3e, 0x11a47, 0x11a47, 0x11a51, 0x11a56, 0x11a59, 0x11a5b, 0x11a8a, 0x11a96,
|
|
285
|
+
0x11a98, 0x11a99, 0x11c30, 0x11c36, 0x11c38, 0x11c3d, 0x11c3f, 0x11c3f, 0x11c92, 0x11ca7,
|
|
286
|
+
0x11caa, 0x11cb0, 0x11cb2, 0x11cb3, 0x11cb5, 0x11cb6, 0x11d31, 0x11d36, 0x11d3a, 0x11d3a,
|
|
287
|
+
0x11d3c, 0x11d3d, 0x11d3f, 0x11d45, 0x11d47, 0x11d47, 0x11d90, 0x11d91, 0x11d95, 0x11d95,
|
|
288
|
+
0x11d97, 0x11d97, 0x11ef3, 0x11ef4, 0x11f00, 0x11f01, 0x11f36, 0x11f3a, 0x11f40, 0x11f40,
|
|
289
|
+
0x11f42, 0x11f42, 0x11f5a, 0x11f5a, 0x13440, 0x13440, 0x13447, 0x13455, 0x1611e, 0x16129,
|
|
290
|
+
0x1612d, 0x1612f, 0x16af0, 0x16af4, 0x16b30, 0x16b36, 0x16f4f, 0x16f4f, 0x16f8f, 0x16f92,
|
|
291
|
+
0x16fe4, 0x16fe4, 0x1bc9d, 0x1bc9e, 0x1cf00, 0x1cf2d, 0x1cf30, 0x1cf46, 0x1d167, 0x1d169,
|
|
292
|
+
0x1d17b, 0x1d182, 0x1d185, 0x1d18b, 0x1d1aa, 0x1d1ad, 0x1d242, 0x1d244, 0x1da00, 0x1da36,
|
|
293
|
+
0x1da3b, 0x1da6c, 0x1da75, 0x1da75, 0x1da84, 0x1da84, 0x1da9b, 0x1da9f, 0x1daa1, 0x1daaf,
|
|
294
|
+
0x1e000, 0x1e006, 0x1e008, 0x1e018, 0x1e01b, 0x1e021, 0x1e023, 0x1e024, 0x1e026, 0x1e02a,
|
|
295
|
+
0x1e08f, 0x1e08f, 0x1e130, 0x1e136, 0x1e2ae, 0x1e2ae, 0x1e2ec, 0x1e2ef, 0x1e4ec, 0x1e4ef,
|
|
296
|
+
0x1e5ee, 0x1e5ef, 0x1e8d0, 0x1e8d6, 0x1e944, 0x1e94a, 0xe0100, 0xe01ef,
|
|
297
|
+
];
|
|
298
|
+
/**
|
|
299
|
+
* East_Asian_Width W or F **and not a space separator**, from UCD 16.0.0. Ambiguous is
|
|
300
|
+
* excluded here on purpose (unlike `WIDE`, which over-counts on purpose for budgeting): on a decision
|
|
301
|
+
* row a character whose width depends on the terminal's mode has no place at all — it escapes.
|
|
302
|
+
* U+3000 is Wide and is a space, so it escapes too: invisible cells are how a row gets padded into
|
|
303
|
+
* an alignment forgery (round-8).
|
|
304
|
+
*/
|
|
305
|
+
const FIXED_WIDE = [
|
|
306
|
+
0x01100, 0x0115f, 0x0231a, 0x0231b, 0x02329, 0x0232a, 0x023e9, 0x023ec, 0x023f0, 0x023f0,
|
|
307
|
+
0x023f3, 0x023f3, 0x025fd, 0x025fe, 0x02614, 0x02615, 0x02630, 0x02637, 0x02648, 0x02653,
|
|
308
|
+
0x0267f, 0x0267f, 0x0268a, 0x0268f, 0x02693, 0x02693, 0x026a1, 0x026a1, 0x026aa, 0x026ab,
|
|
309
|
+
0x026bd, 0x026be, 0x026c4, 0x026c5, 0x026ce, 0x026ce, 0x026d4, 0x026d4, 0x026ea, 0x026ea,
|
|
310
|
+
0x026f2, 0x026f3, 0x026f5, 0x026f5, 0x026fa, 0x026fa, 0x026fd, 0x026fd, 0x02705, 0x02705,
|
|
311
|
+
0x0270a, 0x0270b, 0x02728, 0x02728, 0x0274c, 0x0274c, 0x0274e, 0x0274e, 0x02753, 0x02755,
|
|
312
|
+
0x02757, 0x02757, 0x02795, 0x02797, 0x027b0, 0x027b0, 0x027bf, 0x027bf, 0x02b1b, 0x02b1c,
|
|
313
|
+
0x02b50, 0x02b50, 0x02b55, 0x02b55, 0x02e80, 0x02e99, 0x02e9b, 0x02ef3, 0x02f00, 0x02fd5,
|
|
314
|
+
0x02ff0, 0x02fff, 0x03001, 0x0303e, 0x03041, 0x03096, 0x03099, 0x030ff, 0x03105, 0x0312f,
|
|
315
|
+
0x03131, 0x0318e, 0x03190, 0x031e5, 0x031ef, 0x0321e, 0x03220, 0x03247, 0x03250, 0x0a48c,
|
|
316
|
+
0x0a490, 0x0a4c6, 0x0a960, 0x0a97c, 0x0ac00, 0x0d7a3, 0x0f900, 0x0faff, 0x0fe10, 0x0fe19,
|
|
317
|
+
0x0fe30, 0x0fe52, 0x0fe54, 0x0fe66, 0x0fe68, 0x0fe6b, 0x0ff01, 0x0ff60, 0x0ffe0, 0x0ffe6,
|
|
318
|
+
0x16fe0, 0x16fe4, 0x16ff0, 0x16ff1, 0x17000, 0x187f7, 0x18800, 0x18cd5, 0x18cff, 0x18d08,
|
|
319
|
+
0x1aff0, 0x1aff3, 0x1aff5, 0x1affb, 0x1affd, 0x1affe, 0x1b000, 0x1b122, 0x1b132, 0x1b132,
|
|
320
|
+
0x1b150, 0x1b152, 0x1b155, 0x1b155, 0x1b164, 0x1b167, 0x1b170, 0x1b2fb, 0x1d300, 0x1d356,
|
|
321
|
+
0x1d360, 0x1d376, 0x1f004, 0x1f004, 0x1f0cf, 0x1f0cf, 0x1f18e, 0x1f18e, 0x1f191, 0x1f19a,
|
|
322
|
+
0x1f200, 0x1f202, 0x1f210, 0x1f23b, 0x1f240, 0x1f248, 0x1f250, 0x1f251, 0x1f260, 0x1f265,
|
|
323
|
+
0x1f300, 0x1f320, 0x1f32d, 0x1f335, 0x1f337, 0x1f37c, 0x1f37e, 0x1f393, 0x1f3a0, 0x1f3ca,
|
|
324
|
+
0x1f3cf, 0x1f3d3, 0x1f3e0, 0x1f3f0, 0x1f3f4, 0x1f3f4, 0x1f3f8, 0x1f43e, 0x1f440, 0x1f440,
|
|
325
|
+
0x1f442, 0x1f4fc, 0x1f4ff, 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567, 0x1f57a, 0x1f57a,
|
|
326
|
+
0x1f595, 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f, 0x1f680, 0x1f6c5, 0x1f6cc, 0x1f6cc,
|
|
327
|
+
0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d7, 0x1f6dc, 0x1f6df, 0x1f6eb, 0x1f6ec, 0x1f6f4, 0x1f6fc,
|
|
328
|
+
0x1f7e0, 0x1f7eb, 0x1f7f0, 0x1f7f0, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947, 0x1f9ff,
|
|
329
|
+
0x1fa70, 0x1fa7c, 0x1fa80, 0x1fa89, 0x1fa8f, 0x1fac6, 0x1face, 0x1fadc, 0x1fadf, 0x1fae9,
|
|
330
|
+
0x1faf0, 0x1faf8, 0x20000, 0x2fffd, 0x30000, 0x3fffd,
|
|
331
|
+
];
|
|
332
|
+
const isFixedWide = (cp) => inRanges(FIXED_WIDE, cp);
|
|
333
|
+
/**
|
|
334
|
+
* Characters that are INVISIBLE or MOVE THE CURSOR, from UCD 16.0.0: categories Cc, Cf,
|
|
335
|
+
* Cs, Zl, Zp, every Zs except the space bar, and the variation selectors (U+FE00-FE0F and the
|
|
336
|
+
* supplement U+E0100-E01EF, which `TAG_BLOCK` does not cover).
|
|
337
|
+
*
|
|
338
|
+
* Round-10 swept all 1,112,064 code points and found 2,068 of these still printed RAW and invisible
|
|
339
|
+
* by `stripControl` while the row escaped them — including U+FE0F and the whole variation-selector
|
|
340
|
+
* supplement. This module's own comment names that threat for TAG characters: "the operator reads a
|
|
341
|
+
* benign address while the grant's scope pattern is the RAW target". `[v]` escapes them too now.
|
|
342
|
+
*
|
|
343
|
+
* `[v]` does NOT escape everything the row does, and that asymmetry is deliberate: the row needs
|
|
344
|
+
* every character's width to be fixed, so it also escapes Ambiguous-width and combining characters.
|
|
345
|
+
* `[v]` is folded, not budgeted, so a visible glyph can stay visible there — including a combining
|
|
346
|
+
* mark, which composes onto a base the operator can see.
|
|
347
|
+
*/
|
|
348
|
+
const INVISIBLE = [
|
|
349
|
+
0x00000, 0x0001f, 0x0007f, 0x000a0, 0x000ad, 0x000ad, 0x00600, 0x00605, 0x0061c, 0x0061c,
|
|
350
|
+
0x006dd, 0x006dd, 0x0070f, 0x0070f, 0x00890, 0x00891, 0x008e2, 0x008e2, 0x01680, 0x01680,
|
|
351
|
+
0x0180e, 0x0180e, 0x02000, 0x0200f, 0x02028, 0x0202f, 0x0205f, 0x02064, 0x02066, 0x0206f,
|
|
352
|
+
0x03000, 0x03000, 0x0d800, 0x0dfff, 0x0fe00, 0x0fe0f, 0x0feff, 0x0feff, 0x0fff9, 0x0fffb,
|
|
353
|
+
0x110bd, 0x110bd, 0x110cd, 0x110cd, 0x13430, 0x1343f, 0x1bca0, 0x1bca3, 0x1d173, 0x1d17a,
|
|
354
|
+
0xe0001, 0xe0001, 0xe0020, 0xe007f, 0xe0100, 0xe01ef,
|
|
355
|
+
];
|
|
356
|
+
/** Is `cp` inside one of the flat [lo,hi] pairs? */
|
|
357
|
+
function inRanges(table, cp) {
|
|
358
|
+
let lo = 0;
|
|
359
|
+
let hi = (table.length >> 1) - 1;
|
|
360
|
+
while (lo <= hi) {
|
|
361
|
+
const mid = (lo + hi) >> 1;
|
|
362
|
+
if (cp < table[mid * 2])
|
|
363
|
+
hi = mid - 1;
|
|
364
|
+
else if (cp > table[mid * 2 + 1])
|
|
365
|
+
lo = mid + 1;
|
|
366
|
+
else
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* One line, no control bytes, bounded in terminal COLUMNS — for a field the template puts on a line
|
|
373
|
+
* of its own. `max` is a column budget, not a character count (round-5): the caller is buying screen
|
|
374
|
+
* space, and on this screen a character can cost one column or two.
|
|
375
|
+
*
|
|
376
|
+
* The budget is spent on the OPERATOR'S text: the source is clipped first so escape spam cannot
|
|
377
|
+
* evict the real command (round-3), and the sanitised result is clipped again so markers cannot
|
|
378
|
+
* inflate the output (round-4). The truncation notice is appended AFTER sanitising, so no sequence
|
|
379
|
+
* sitting at the clip boundary can swallow it (round-4).
|
|
380
|
+
*/
|
|
381
|
+
function flattenField(s, max = exports.ROW_COLS) {
|
|
382
|
+
return renderField(s, max).text;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The renderer behind `flattenField`, returning HOW MUCH IT HID as well as what it shows.
|
|
386
|
+
*
|
|
387
|
+
* Round-8 split this out: callers that need to know whether anything was hidden used to test the
|
|
388
|
+
* rendered string against the notice's shape, which the data can imitate. A fact about the render
|
|
389
|
+
* belongs to the renderer.
|
|
390
|
+
*/
|
|
391
|
+
function renderField(s, max = exports.ROW_COLS) {
|
|
392
|
+
// Walked TOKEN BY TOKEN so two round-5 defects cannot exist: the clip can never land inside a
|
|
393
|
+
// marker (markers are emitted whole), and the truncation notice counts SOURCE code points the
|
|
394
|
+
// operator is not seeing — round-4's collapse-then-clip counted the marker-expanded string, which
|
|
395
|
+
// overstated by 4x on one input and understated by 2x on another. On a screen whose job is telling
|
|
396
|
+
// the human how much of the subject is hidden, the number must come from the subject.
|
|
397
|
+
// No TAB expansion here, deliberately: `TOKEN`'s whitespace class already absorbs tabs and this
|
|
398
|
+
// function collapses any whitespace run to one space, so the replace round-7 added was an
|
|
399
|
+
// EQUIVALENT MUTANT — measured over 60,000 input pairs, zero differences. §11's claim that
|
|
400
|
+
// flattenField "expands TAB to one space" was inaccurate; it collapses a run of ten to one space,
|
|
401
|
+
// which is the same visible result by a different mechanism. The expansion that matters is in
|
|
402
|
+
// `stripControl`, where newlines are content and runs are not collapsed. Code with no effect is
|
|
403
|
+
// not defence in depth (round-5's rule about unexercised guards).
|
|
404
|
+
const src = neutraliseMarkers(String(s));
|
|
405
|
+
const total = [...src].length;
|
|
406
|
+
// A COLUMN budget alone does not bound the output, because a combining mark advances zero columns
|
|
407
|
+
// (round-6). Measured: a target of `git status` plus two million U+0301 came back WHOLE from a
|
|
408
|
+
// 52-column call — 2,000,021 code points, 4 MB written to the screen, 590 MB resident, and
|
|
409
|
+
// `truncated` false, so the notice claimed nothing was hidden. That is this module's property 3
|
|
410
|
+
// failing outright. Cells are what the screen shows; code points are what it must still transmit,
|
|
411
|
+
// so both are capped. Eight per column is far past any legitimate stacking (a decomposed Hangul
|
|
412
|
+
// or Vietnamese syllable is two or three).
|
|
413
|
+
const cpCap = max * 8;
|
|
414
|
+
const walk = (budget) => {
|
|
415
|
+
let out = '';
|
|
416
|
+
let outCols = 0;
|
|
417
|
+
let outCps = 0;
|
|
418
|
+
let consumed = 0;
|
|
419
|
+
let pendingSpace = false;
|
|
420
|
+
for (const [tok] of src.matchAll(TOKEN)) {
|
|
421
|
+
const tokLen = [...tok].length;
|
|
422
|
+
if (/^[\r\n\t ]+$/.test(tok)) {
|
|
423
|
+
pendingSpace = out !== '';
|
|
424
|
+
consumed += tokLen;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const piece = REMOVABLE_RUN.test(tok) ? mark(tok) : tok;
|
|
428
|
+
REMOVABLE_RUN.lastIndex = 0;
|
|
429
|
+
const withSpace = (pendingSpace ? ' ' : '') + piece;
|
|
430
|
+
const cost = displayWidth(withSpace);
|
|
431
|
+
const cps = [...withSpace].length;
|
|
432
|
+
if (outCols + cost > budget || outCps + cps > cpCap)
|
|
433
|
+
break;
|
|
434
|
+
out += withSpace;
|
|
435
|
+
outCols += cost;
|
|
436
|
+
outCps += cps;
|
|
437
|
+
pendingSpace = false;
|
|
438
|
+
consumed += tokLen;
|
|
439
|
+
}
|
|
440
|
+
return { out, consumed };
|
|
441
|
+
};
|
|
442
|
+
// The NOTICE IS INSIDE THE BUDGET (round-5). It used to be appended after the walk, so a caller
|
|
443
|
+
// asking for 72 columns got 97 — and a budget that the thing being budgeted can overrun is not a
|
|
444
|
+
// budget. Its width depends on how much was hidden, which depends on the budget, so the walk is
|
|
445
|
+
// repeated until it settles (it settles at once in every real case; the loop is for the digit
|
|
446
|
+
// boundaries where hiding one more character lengthens the number).
|
|
447
|
+
let budget = max;
|
|
448
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
449
|
+
const r = walk(budget);
|
|
450
|
+
if (r.consumed >= total)
|
|
451
|
+
return { text: r.out, hidden: 0 }; // nothing hidden, nothing to say
|
|
452
|
+
const hidden = total - r.consumed;
|
|
453
|
+
const notice = shortNotice(max, hidden);
|
|
454
|
+
if (displayWidth(r.out) + displayWidth(notice) <= max)
|
|
455
|
+
return { text: r.out + notice, hidden };
|
|
456
|
+
budget = max - displayWidth(notice);
|
|
457
|
+
}
|
|
458
|
+
// A budget too small to hold even the notice: say the honest thing rather than a silent nothing.
|
|
459
|
+
// Callers on this screen pass at least 20; this is the guard, not a path.
|
|
460
|
+
return { text: shortNotice(max, total), hidden: total };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* @implements A-SPEC-262.1 §13
|
|
464
|
+
* The form agent-controlled text takes on a DECISION ROW: only characters whose width is fixed by
|
|
465
|
+
* the standard, everything else as an ASCII escape.
|
|
466
|
+
*
|
|
467
|
+
* Eight adversarial rounds found the same shape of defect eight times — a character whose width the
|
|
468
|
+
* code guessed (Ambiguous, combining, zero-width), or whose cursor effect it did not model (TAB,
|
|
469
|
+
* ESC), or whose position let the data choose a line boundary (alignment, folding). None of them can
|
|
470
|
+
* occur here, because none of those characters survive to the row: a control byte is not stripped
|
|
471
|
+
* and counted, it is SHOWN as `\u001b`. Nothing is removed, so nothing is hidden — which also
|
|
472
|
+
* retires the removal marker, its forgery guard and its size accounting from this path entirely.
|
|
473
|
+
*
|
|
474
|
+
* What passes through: printable ASCII (one column each) and East_Asian_Width W or F (two columns
|
|
475
|
+
* each, fixed by the standard). Korean and CJK commands therefore stay readable on the screen the
|
|
476
|
+
* operator decides from — the requirement that made this a hybrid rather than a full escaping.
|
|
477
|
+
* A backslash is doubled first, so data can never spell an escape the renderer did not write.
|
|
478
|
+
*/
|
|
479
|
+
function rowLiteral(s) {
|
|
480
|
+
return rowPieces(s).map((p) => p.text).join('');
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* @implements A-SPEC-262.1 §13
|
|
484
|
+
* The row's pieces: one per source character, except that a run of the same escaped code point
|
|
485
|
+
* becomes one piece carrying its count. ONE builder, so `rowLiteral` and `rowField` cannot render
|
|
486
|
+
* the same value two different ways — round-10 found the un-collapsed form ambiguous where the
|
|
487
|
+
* collapsed one was not, which is the same defect twice with different reachability.
|
|
488
|
+
*
|
|
489
|
+
* The notation belongs to the renderer. `*` and digits are printable ASCII, so the DATA could
|
|
490
|
+
* supply them: `chmod` + two U+200B + `777 /etc/shadow` rendered `chmod \u200b*2777 /etc/shadow`,
|
|
491
|
+
* absorbing the mode argument into the count — a removal on a decision row, with no notice and no
|
|
492
|
+
* escape byte, and two different sources producing identical rows. Doubling backslashes guarded the
|
|
493
|
+
* wrong half; what must be escaped is whatever could EXTEND the renderer's own token.
|
|
494
|
+
*/
|
|
495
|
+
/** One source character as it appears on a decision row. Width: 1 per ASCII char, 2 per W/F char. */
|
|
496
|
+
function rowLiteralChar(ch) {
|
|
497
|
+
const cp = ch.codePointAt(0);
|
|
498
|
+
if (ch === '\\')
|
|
499
|
+
return '\\\\';
|
|
500
|
+
if (cp >= 0x20 && cp <= 0x7e)
|
|
501
|
+
return ch;
|
|
502
|
+
// Wide and Fullwidth only — never Ambiguous, and never a zero-advance mark, even a wide-block one.
|
|
503
|
+
if (!inRanges(ZERO, cp) && isFixedWide(cp))
|
|
504
|
+
return ch;
|
|
505
|
+
return cp > 0xffff
|
|
506
|
+
? `\\u{${cp.toString(16)}}`
|
|
507
|
+
: `\\u${cp.toString(16).padStart(4, '0')}`;
|
|
508
|
+
}
|
|
509
|
+
function rowPieces(value) {
|
|
510
|
+
const pieces = [];
|
|
511
|
+
let prevWasEscape = false;
|
|
512
|
+
let prevWasCount = false;
|
|
513
|
+
for (const ch of String(value)) {
|
|
514
|
+
let piece = rowLiteralChar(ch);
|
|
515
|
+
if (piece === '*' && prevWasEscape)
|
|
516
|
+
piece = '\\u002a';
|
|
517
|
+
else if (prevWasCount && piece >= '0' && piece <= '9')
|
|
518
|
+
piece = `\\u003${piece}`;
|
|
519
|
+
const prev = pieces[pieces.length - 1];
|
|
520
|
+
if (prev && prev.piece === piece && piece.startsWith('\\') && piece !== '\\\\') {
|
|
521
|
+
prev.chars += 1;
|
|
522
|
+
prev.text = `${piece}*${prev.chars}`;
|
|
523
|
+
prevWasEscape = true;
|
|
524
|
+
prevWasCount = true;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
pieces.push({ text: piece, chars: 1, piece });
|
|
528
|
+
prevWasEscape = piece.startsWith('\\') && piece !== '\\\\';
|
|
529
|
+
prevWasCount = false;
|
|
530
|
+
}
|
|
531
|
+
return pieces;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* @implements A-SPEC-262.1 §13
|
|
535
|
+
* A decision row's field: the row literal, cut to `max` columns, with what it could not show stated.
|
|
536
|
+
*/
|
|
537
|
+
function rowField(value, max = exports.ROW_COLS) {
|
|
538
|
+
return renderRow(value, max).text;
|
|
539
|
+
}
|
|
540
|
+
function renderRow(value, max) {
|
|
541
|
+
const chars = [...String(value)];
|
|
542
|
+
const pieces = rowPieces(value);
|
|
543
|
+
const fits = (budget) => {
|
|
544
|
+
let text = '';
|
|
545
|
+
let cols = 0;
|
|
546
|
+
let used = 0;
|
|
547
|
+
for (const p of pieces) {
|
|
548
|
+
const w = displayWidth(p.text);
|
|
549
|
+
if (cols + w > budget)
|
|
550
|
+
break;
|
|
551
|
+
text += p.text;
|
|
552
|
+
cols += w;
|
|
553
|
+
used += p.chars;
|
|
554
|
+
}
|
|
555
|
+
return { text, used };
|
|
556
|
+
};
|
|
557
|
+
let budget = max;
|
|
558
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
559
|
+
const r = fits(budget);
|
|
560
|
+
if (r.used >= chars.length)
|
|
561
|
+
return { text: r.text, hidden: 0 };
|
|
562
|
+
const hidden = chars.length - r.used;
|
|
563
|
+
const notice = shortNotice(max, hidden);
|
|
564
|
+
if (r.text !== '' && displayWidth(r.text) + displayWidth(notice) <= max) {
|
|
565
|
+
return { text: r.text + notice, hidden };
|
|
566
|
+
}
|
|
567
|
+
budget = max - displayWidth(notice);
|
|
568
|
+
}
|
|
569
|
+
// Round-9: a cell whose budget cannot hold one piece AND the notice used to render as the notice
|
|
570
|
+
// alone — the category of authority reading `…(99999자)` and nothing else, which is the
|
|
571
|
+
// concealment round-5 named. One piece is always shown; the notice shrinks to whatever is left,
|
|
572
|
+
// and if even that does not fit the row states the count with no room wasted on punctuation.
|
|
573
|
+
const first = pieces[0];
|
|
574
|
+
if (!first)
|
|
575
|
+
return { text: '', hidden: 0 };
|
|
576
|
+
const total = chars.length;
|
|
577
|
+
if (first.chars >= total && displayWidth(first.text) <= max)
|
|
578
|
+
return { text: first.text, hidden: 0 };
|
|
579
|
+
// Three properties, in this order, because they cannot all hold in eight columns (round-10):
|
|
580
|
+
// 1. NEVER WIDER THAN THE BUDGET — a row that overflows takes the screen with it, which is the
|
|
581
|
+
// harm every round since the fifth has been about. This one is absolute.
|
|
582
|
+
// 2. NEVER HIDE SILENTLY — §13 promises the clip is stated. The notice shrinks rather than
|
|
583
|
+
// disappears: the count, then a bare ellipsis, which claims nothing it cannot support.
|
|
584
|
+
// 3. SHOW SOMETHING — a bound met by showing nothing is the concealment round-5 named.
|
|
585
|
+
// Round-9 had (3) and dropped (2); the first attempt at (2) dropped (3) and then broke (1) by
|
|
586
|
+
// printing a sixteen-column piece into eight columns.
|
|
587
|
+
const notices = (hidden) => [shortNotice(max, hidden), `…${hidden}`, '…'];
|
|
588
|
+
const withFirst = total - first.chars;
|
|
589
|
+
for (const notice of notices(withFirst)) {
|
|
590
|
+
if (displayWidth(first.text) + displayWidth(notice) <= max) {
|
|
591
|
+
return { text: first.text + notice, hidden: withFirst };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const notice of notices(total)) {
|
|
595
|
+
if (displayWidth(notice) <= max)
|
|
596
|
+
return { text: notice, hidden: total };
|
|
597
|
+
}
|
|
598
|
+
return { text: '', hidden: total };
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Fold a sanitised string onto rows of at most `cols` columns, preferring space boundaries.
|
|
602
|
+
*
|
|
603
|
+
* Round-5: `[v]`'s whole job is showing the subject WHOLE, so clipping it to a row is the wrong
|
|
604
|
+
* answer there — but leaving it on one 8,000-column line is the same flood by another name. Folding
|
|
605
|
+
* keeps both: nothing is hidden and no line wraps unpredictably. Existing newlines are preserved
|
|
606
|
+
* (they are the body's own structure); a token longer than a row is hard-broken, since a URL with
|
|
607
|
+
* no spaces must still be readable.
|
|
608
|
+
*
|
|
609
|
+
* `indent` is a LEFT MARGIN THE TEMPLATE OWNS, and it is not decoration. Round-6: folding hands the
|
|
610
|
+
* choice of line boundary to the data, so a target padded to exact 76-column multiples put forged
|
|
611
|
+
* rows — `[1] shell git status`, `◆ 승인 대기 2건` — at column 0 of the `[v]` screen with ZERO escape
|
|
612
|
+
* bytes (measured on a real pty). The terminal's own wrapping had the same hole before folding
|
|
613
|
+
* existed. A margin the data cannot reach means no data byte ever starts a line, so nothing it emits
|
|
614
|
+
* can pose as a row this screen wrote.
|
|
615
|
+
*/
|
|
616
|
+
/**
|
|
617
|
+
* Split a token into ATOMIC display units for hard-breaking: an escape sequence (`\uXXXX`,
|
|
618
|
+
* `\u{XXXXX}`), a doubled backslash, or one code point. Round-12: the tokenizer's escape
|
|
619
|
+
* alternatives only win when an escape STARTS a token, so an escape embedded in a long unbroken
|
|
620
|
+
* `\S+` run (`installer…x86_64ZZ\u00a0and-curl-evil`) was swallowed whole, and the char-by-char
|
|
621
|
+
* hard-break then cut inside it — `\u00` at a row's end, `a0` at the next's start. That is round-1's
|
|
622
|
+
* severed representation, in the `[v]` view a clipped row sends the operator to. Breaking by unit
|
|
623
|
+
* instead keeps every escape whole; only an escape that alone exceeds `room` (room=8 degenerate) is
|
|
624
|
+
* split, and it is split as a last resort, not by default.
|
|
625
|
+
*/
|
|
626
|
+
function atomicUnits(tok) {
|
|
627
|
+
return tok.match(/\\u\{[0-9a-f]+\}|\\u[0-9a-f]{4}|\\\\|[\s\S]/gu) ?? [];
|
|
628
|
+
}
|
|
629
|
+
function wrapColumns(s, cols = 76, indent = '') {
|
|
630
|
+
const rows = [];
|
|
631
|
+
const room = Math.max(8, cols - displayWidth(indent));
|
|
632
|
+
for (const para of String(s).split('\n')) {
|
|
633
|
+
let row = '';
|
|
634
|
+
let width = 0;
|
|
635
|
+
const flush = () => { rows.push(row); row = ''; width = 0; };
|
|
636
|
+
// Round-10: an escape is ONE token. Folding used to hard-break inside `\u00a0`, leaving `\u` at
|
|
637
|
+
// the end of a row and `00a0` at the start of the next — the severed-surrogate lesson of round-1
|
|
638
|
+
// in the representation round-9 introduced. It split the truncation notice too, so a caller
|
|
639
|
+
// reading the screen could not even find the number.
|
|
640
|
+
for (const [tok] of para.matchAll(/\\u\{[0-9a-f]+\}|\\u[0-9a-f]{4}|\\\\|…\s*\([^)]*\)|\s+|\S+/gu)) {
|
|
641
|
+
const w = displayWidth(tok);
|
|
642
|
+
if (/^\s+$/.test(tok)) {
|
|
643
|
+
// Round-7: a run that did not fit was DROPPED here, silently. An aligned table in a spec
|
|
644
|
+
// body lost 120 of its 173 spaces and its columns merged into one — property 2 ("nothing
|
|
645
|
+
// vanishes silently") failing with no marker and no notice, on the screen whose job is
|
|
646
|
+
// showing the subject as it is. Whitespace is content; it folds like everything else.
|
|
647
|
+
// Round-8: this loop advanced by ONE per character, so U+3000 IDEOGRAPHIC SPACE — East
|
|
648
|
+
// Asian Wide, two cells — overflowed the fold and put data at physical column 0 again, the
|
|
649
|
+
// very forgery round-6's margin and round-7's tab expansion were added to stop. Round-7
|
|
650
|
+
// wrote this branch while fixing the silent-deletion bug and hardcoded the width it had just
|
|
651
|
+
// spent the round proving could not be assumed. Whitespace is measured like everything else.
|
|
652
|
+
for (const ch of tok) {
|
|
653
|
+
const cw = displayWidth(ch);
|
|
654
|
+
if (width + cw > room)
|
|
655
|
+
flush();
|
|
656
|
+
row += ch;
|
|
657
|
+
width += cw;
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (w > room) { // hard-break an unbreakable token
|
|
662
|
+
// By atomic UNIT, not code point (round-12): an escape embedded in this run must not be cut
|
|
663
|
+
// in half. A unit that alone exceeds room (only possible when room is at its 8-col floor)
|
|
664
|
+
// still falls back to a code-point break — unavoidable, and it cannot forge a row because
|
|
665
|
+
// the margin owns column 0.
|
|
666
|
+
for (const unit of atomicUnits(tok)) {
|
|
667
|
+
const uw = displayWidth(unit);
|
|
668
|
+
if (uw > room) {
|
|
669
|
+
for (const ch of unit) {
|
|
670
|
+
const cw = displayWidth(ch);
|
|
671
|
+
if (width + cw > room)
|
|
672
|
+
flush();
|
|
673
|
+
row += ch;
|
|
674
|
+
width += cw;
|
|
675
|
+
}
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (width + uw > room)
|
|
679
|
+
flush();
|
|
680
|
+
row += unit;
|
|
681
|
+
width += uw;
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (width + w > room)
|
|
686
|
+
flush();
|
|
687
|
+
row += tok;
|
|
688
|
+
width += w;
|
|
689
|
+
}
|
|
690
|
+
rows.push(row);
|
|
691
|
+
}
|
|
692
|
+
return rows.map((r) => indent + r).join('\n');
|
|
693
|
+
}
|
|
694
|
+
/** Newlines survive (they are the body); everything that moves the cursor or paints does not. */
|
|
695
|
+
/**
|
|
696
|
+
* Space separators that are not the space bar. Round-9: the decision row escapes these (§13 excludes
|
|
697
|
+
* them from FIXED_WIDE because invisible cells are how a row gets padded into a forgery), while
|
|
698
|
+
* `[v]` — the view the row SENDS the operator to when it clips — printed them raw and unmarked, so
|
|
699
|
+
* `rm -rf\u00a0/` read there exactly like `rm -rf /` though the argument is different. The fuller
|
|
700
|
+
* view must not be the less honest one; they are escaped here too.
|
|
701
|
+
*
|
|
702
|
+
* TAB joins them. Round-7 replaced it with one space, which already destroyed the indentation it
|
|
703
|
+
* was carrying, so the escape costs nothing in readability and says what the byte actually is.
|
|
704
|
+
*/
|
|
705
|
+
const ODD_SPACES = /[\t\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]/gu;
|
|
706
|
+
function stripControl(s) {
|
|
707
|
+
let out = '';
|
|
708
|
+
for (const ch of String(s)) {
|
|
709
|
+
if (ch === '\n') {
|
|
710
|
+
out += ch;
|
|
711
|
+
continue;
|
|
712
|
+
} // the body's own structure
|
|
713
|
+
if (ch === '\r') {
|
|
714
|
+
out += '\n';
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (ch === '\\') {
|
|
718
|
+
out += '\\\\';
|
|
719
|
+
continue;
|
|
720
|
+
} // round-10: so data cannot spell an escape
|
|
721
|
+
const cp = ch.codePointAt(0);
|
|
722
|
+
if (!inRanges(INVISIBLE, cp)) {
|
|
723
|
+
out += ch;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
out += cp > 0xffff
|
|
727
|
+
? `\\u{${cp.toString(16)}}`
|
|
728
|
+
: `\\u${cp.toString(16).padStart(4, '0')}`;
|
|
729
|
+
}
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Clip on a CODE POINT boundary. Round-1: slicing UTF-16 units cut a surrogate pair in half and the
|
|
734
|
+
* last visible character of a clipped command rendered as a replacement glyph — on a screen whose
|
|
735
|
+
* whole job is showing the subject verbatim.
|
|
736
|
+
*/
|
|
737
|
+
function clipCodePoints(s, max) {
|
|
738
|
+
const cps = [...s];
|
|
739
|
+
return cps.length <= max ? s : cps.slice(0, max).join('') + `… (${cps.length - max}자 잘림)`;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Quote a value that is about to appear inside a COPY-PASTEABLE command line.
|
|
743
|
+
*
|
|
744
|
+
* Round-2: the non-TTY hint (A-SPEC-260) fills the real queue id into three `holmes-kit approve …`
|
|
745
|
+
* lines whose whole purpose is being pasted into a shell — and the id is agent-controlled. Flattening
|
|
746
|
+
* stops it forging lines, but a flattened `… | sh` still PIPES when pasted. POSIX single quotes make
|
|
747
|
+
* any byte inert; a literal quote is closed, escaped and reopened.
|
|
748
|
+
*/
|
|
749
|
+
function shellQuote(s) {
|
|
750
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* A reference for a pasteable command line: left ALONE when it is already inert (the shape a real
|
|
754
|
+
* queue id has), quoted only when it is not. Round-2 wanted every pasted byte harmless; quoting
|
|
755
|
+
* unconditionally would also have rewritten the A-SPEC-260 hint that ships today, so the blast
|
|
756
|
+
* radius is kept to the case that needs it — a normal session's screen is byte-for-byte unchanged.
|
|
757
|
+
*/
|
|
758
|
+
function safeRef(s) {
|
|
759
|
+
return /^[A-Za-z0-9_.:@/-]+$/.test(s) ? s : shellQuote(s);
|
|
760
|
+
}
|