@adea-ai/ui 0.66.1 → 0.67.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/README.md +21 -0
- package/dist/NOTICE +24 -0
- package/dist/components/conversation/index.d.ts +1 -0
- package/dist/components/conversation/index.d.ts.map +1 -1
- package/dist/components/conversation/index.js +2 -1
- package/dist/components/conversation/paste-tokens.d.ts +57 -0
- package/dist/components/conversation/paste-tokens.d.ts.map +1 -0
- package/dist/components/conversation/paste-tokens.js +216 -0
- package/dist/components/conversation/paste-tokens.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/r/conversation.json +5 -0
- package/dist/r/registry.json +5 -0
- package/dist/r/src/components/conversation/index.ts +19 -0
- package/dist/r/src/components/conversation/paste-tokens.ts +272 -0
- package/package.json +9 -2
- package/registry.json +5 -0
- package/src/components/conversation/index.ts +19 -0
- package/src/components/conversation/paste-tokens.ts +272 -0
package/README.md
CHANGED
|
@@ -373,6 +373,15 @@ imports and a 3 KiB gzip model budget (measured baseline: 2,455 bytes). This is
|
|
|
373
373
|
model-subpath evidence; it does not waive UI #16's package attribution/conditional
|
|
374
374
|
export corrections or the separate root compatibility gate.
|
|
375
375
|
|
|
376
|
+
`bun run check:packed-paste-model` installs the actual archive with peer and
|
|
377
|
+
optional packages omitted. It verifies the direct
|
|
378
|
+
`@adea-ai/ui/components/conversation/paste-tokens` entry in native Node and the
|
|
379
|
+
Solid source condition, a strict clean TypeScript consumer without ambient types,
|
|
380
|
+
round-trip/range/pruning and invalid-metadata behavior, and exact packed
|
|
381
|
+
LICENSE/NOTICE fidelity. The pure compiled module retains zero runtime imports
|
|
382
|
+
and stays within a 3 KiB gzip budget. This gate runs in local verification, CI
|
|
383
|
+
and publication; textarea editing and production adoption remain separate gates.
|
|
384
|
+
|
|
376
385
|
### Controlled appearance editor
|
|
377
386
|
|
|
378
387
|
`AppearanceEditor` and `AppearancePopover` retain the accepted Zeron-derived
|
|
@@ -422,3 +431,15 @@ not just the editor primitive.
|
|
|
422
431
|
Local packed interaction gates require `bunx playwright install chromium webkit`
|
|
423
432
|
after dependency installation. On Linux, use `--with-deps` when the required
|
|
424
433
|
system libraries are absent. All automation runs headless with disposable profiles.
|
|
434
|
+
|
|
435
|
+
## Paste-token model (issue #532 selected unit)
|
|
436
|
+
|
|
437
|
+
`@adea-ai/ui/components/conversation` also exports the pure paste-token model:
|
|
438
|
+
`PasteBlock`, `isPasteBlock`, `countLines`, `shouldCollapse`, `nextSeq`,
|
|
439
|
+
`formatToken`, `findTokenRanges`, `tokenRangeAt`, `pruneBlocks`, `expandAll`,
|
|
440
|
+
`recollapsePastes`, `remapCarriedBlocks`, `stripTrailingBlankLines`, and the
|
|
441
|
+
threshold and marker constants. Hosts provide stable, bounded ASCII block IDs and own
|
|
442
|
+
draft state, persistence, message reconciliation and editor behavior. The model
|
|
443
|
+
does not read storage or the clipboard, upload files, edit a textarea, or
|
|
444
|
+
provide undo/history, previews or chips. Its selected source and Apache-2.0
|
|
445
|
+
attribution are recorded in the issue #532 entry in `NOTICE`.
|
package/dist/NOTICE
CHANGED
|
@@ -267,3 +267,27 @@ pane-title gesture, replacing plaintext leaf IDs and string casts with typed
|
|
|
267
267
|
intent, an instance-local active drag payload, and cancellation/removal checks.
|
|
268
268
|
Header action composition injects application keyboard controls without owning
|
|
269
269
|
shortcuts or domain transitions. No new upstream source or license is introduced.
|
|
270
|
+
## Paste-token model translated from KiroCrew (issue #532)
|
|
271
|
+
|
|
272
|
+
`packages/ui/src/components/conversation/paste-tokens.ts` translates the pure
|
|
273
|
+
block and text transformations from `website/src/utils/pasteTokens.ts`, plus
|
|
274
|
+
the trailing-blank-line policy from `website/src/components/composerPastePolicy.ts`,
|
|
275
|
+
at KiroCrew revision `283e136c0f902e965a535a7c9548c57c7504fed0`.
|
|
276
|
+
|
|
277
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
278
|
+
This product includes software developed at Amazon.com, Inc.
|
|
279
|
+
(https://www.amazon.com/). Licensed under the Apache License, Version 2.0.
|
|
280
|
+
|
|
281
|
+
Retained: the three-line/200-character collapse thresholds; canonical marker
|
|
282
|
+
formatting and ranges; monotonic sequence allocation and collision remapping;
|
|
283
|
+
token pruning, expansion and content-based re-collapse, including the
|
|
284
|
+
trailing-whitespace fallback; and the linear trailing-blank-line scan. Bun unit
|
|
285
|
+
cases adapt the pure expectations from `website/src/test/pasteTokens.test.ts` and
|
|
286
|
+
the trailing-text policy from `website/src/test/ChatInput.paste.test.tsx`.
|
|
287
|
+
|
|
288
|
+
The host supplies stable block IDs. Invalid IDs/sequences, duplicate identities,
|
|
289
|
+
ambiguous sequence mappings and noncanonical or line-mismatched markers are
|
|
290
|
+
refused. React, Lexical, browser clipboard/File handling, localStorage side-table
|
|
291
|
+
recovery, backend message reconciliation, uploads, textarea editing, previews,
|
|
292
|
+
chips, undo/history and submit/persistence authority are not included. This
|
|
293
|
+
selected model is not rich editing, complete Chat, or application adoption.
|
|
@@ -5,4 +5,5 @@ export { ConversationSurface, type ConversationSurfaceProps } from './conversati
|
|
|
5
5
|
export { ComposerAttachmentButton, ComposerHint, MessageComposer, type MessageComposerProps, } from './message-composer';
|
|
6
6
|
export { MessageBody, MessageDayDivider, MessageGroup, MessageRow, type MessageRowProps, } from './message-row';
|
|
7
7
|
export { AttachmentCard, ThreadPanel, type AttachmentCardProps, type ThreadPanelProps, } from './thread-panel';
|
|
8
|
+
export { countLines, expandAll, findTokenRanges, formatToken, isPasteBlock, nextSeq, pruneBlocks, recollapsePastes, remapCarriedBlocks, shouldCollapse, stripTrailingBlankLines, tokenRangeAt, PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, type PasteBlock, } from './paste-tokens';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAChG,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAC3F,OAAO,EACL,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,cAAc,EACd,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAChG,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAC3F,OAAO,EACL,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,cAAc,EACd,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,YAAY,EACZ,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAA"}
|
|
@@ -5,4 +5,5 @@ import { ConversationSurface } from "./conversation-surface.js";
|
|
|
5
5
|
import { ComposerAttachmentButton, ComposerHint, MessageComposer } from "./message-composer.js";
|
|
6
6
|
import { MessageBody, MessageDayDivider, MessageGroup, MessageRow } from "./message-row.js";
|
|
7
7
|
import { AttachmentCard, ThreadPanel } from "./thread-panel.js";
|
|
8
|
-
|
|
8
|
+
import { PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, countLines, expandAll, findTokenRanges, formatToken, isPasteBlock, nextSeq, pruneBlocks, recollapsePastes, remapCarriedBlocks, shouldCollapse, stripTrailingBlankLines, tokenRangeAt } from "./paste-tokens.js";
|
|
9
|
+
export { AttachmentCard, BusySendButton, ChatComposer, ComposerAttachmentButton, ComposerHint, ConversationAvatar, ConversationSurface, MessageBody, MessageComposer, MessageDayDivider, MessageGroup, MessageRow, PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, ThreadPanel, countLines, expandAll, findTokenRanges, formatToken, isPasteBlock, nextSeq, pruneBlocks, recollapsePastes, remapCarriedBlocks, shouldCollapse, stripTrailingBlankLines, tokenRangeAt };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure paste-token transformations translated and hardened from KiroCrew's
|
|
3
|
+
* `website/src/utils/pasteTokens.ts` at revision
|
|
4
|
+
* `283e136c0f902e965a535a7c9548c57c7504fed0` (Apache-2.0). See the #532 entry
|
|
5
|
+
* in the repository NOTICE. Paste identity is supplied by the host; this module
|
|
6
|
+
* does not access storage, clipboard APIs, message services, or editor state.
|
|
7
|
+
*/
|
|
8
|
+
/** A collapsed paste block stored alongside input or a message. */
|
|
9
|
+
export interface PasteBlock {
|
|
10
|
+
/** A stable host-provided ASCII identifier; never included in visible token text. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Positive safe integer, unique among blocks in the same text value. */
|
|
13
|
+
seq: number;
|
|
14
|
+
/** Positive safe integer displayed in the token. */
|
|
15
|
+
lines: number;
|
|
16
|
+
/** Original pasted text, retained verbatim. */
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const PASTE_THRESHOLD_LINES = 3;
|
|
20
|
+
export declare const PASTE_THRESHOLD_CHARS = 200;
|
|
21
|
+
/** Canonical token pattern. Internal scans always use a fresh RegExp instance. */
|
|
22
|
+
export declare const PASTE_TOKEN_REGEX: RegExp;
|
|
23
|
+
/** Check data received from a host before using it as a paste-token block. */
|
|
24
|
+
export declare function isPasteBlock(value: unknown): value is PasteBlock;
|
|
25
|
+
export declare function formatToken(block: PasteBlock): string;
|
|
26
|
+
export declare function shouldCollapse(text: string): boolean;
|
|
27
|
+
export declare function countLines(text: string): number;
|
|
28
|
+
/** Next sequence for a new paste = max existing + 1, starting at 1. */
|
|
29
|
+
export declare function nextSeq(blocks: readonly PasteBlock[]): number;
|
|
30
|
+
/**
|
|
31
|
+
* Re-sequence `carried` blocks whose sequence is already taken by `used`, and
|
|
32
|
+
* rewrite their markers in one right-to-left pass. Carried block IDs and
|
|
33
|
+
* sequences must be unique so a visible marker always has one backing block.
|
|
34
|
+
* `used` is mutated to include the assigned sequences.
|
|
35
|
+
*/
|
|
36
|
+
export declare function remapCarriedBlocks(text: string, carried: readonly PasteBlock[], used: Set<number>): {
|
|
37
|
+
text: string;
|
|
38
|
+
blocks: PasteBlock[];
|
|
39
|
+
};
|
|
40
|
+
/** Ranges for canonical tokens whose sequence and line count match one block. */
|
|
41
|
+
export declare function findTokenRanges(text: string, blocks: readonly PasteBlock[]): Array<{
|
|
42
|
+
start: number;
|
|
43
|
+
end: number;
|
|
44
|
+
block: PasteBlock;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function tokenRangeAt(text: string, blocks: readonly PasteBlock[], caret: number): {
|
|
47
|
+
start: number;
|
|
48
|
+
end: number;
|
|
49
|
+
block: PasteBlock;
|
|
50
|
+
} | null;
|
|
51
|
+
export declare function pruneBlocks(text: string, blocks: PasteBlock[]): PasteBlock[];
|
|
52
|
+
export declare function expandAll(text: string, blocks: readonly PasteBlock[]): string;
|
|
53
|
+
/** Inverse of `expandAll`: replace each first non-overlapping verbatim block. */
|
|
54
|
+
export declare function recollapsePastes(content: string, blocks: readonly PasteBlock[]): string;
|
|
55
|
+
/** Strip a trailing whitespace run only when that run contains a newline. */
|
|
56
|
+
export declare function stripTrailingBlankLines(value: string): string;
|
|
57
|
+
//# sourceMappingURL=paste-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paste-tokens.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/paste-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAA;IACV,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,qBAAqB,IAAI,CAAA;AACtC,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAMxC,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAsC,CAAA;AAMpE,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAUhE;AAaD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAGrD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAO7D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAA;CAAE,CAiDxC;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,UAAU,EAAE,GAC5B,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC,CAkB1D;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,KAAK,EAAE,MAAM,GACZ;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG,IAAI,CAM1D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAQ5E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAU7E;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAgDvF;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB7D"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region src/components/conversation/paste-tokens.ts
|
|
2
|
+
var PASTE_THRESHOLD_LINES = 3;
|
|
3
|
+
var PASTE_THRESHOLD_CHARS = 200;
|
|
4
|
+
var PASTE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
5
|
+
var MAX_SAFE_SEQUENCE_TEXT_LENGTH = String(Number.MAX_SAFE_INTEGER).length;
|
|
6
|
+
var PASTE_TOKEN_SOURCE = String.raw`\[ Paste #([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) · ([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) lines \]`;
|
|
7
|
+
/** Canonical token pattern. Internal scans always use a fresh RegExp instance. */
|
|
8
|
+
var PASTE_TOKEN_REGEX = new RegExp(PASTE_TOKEN_SOURCE, "g");
|
|
9
|
+
function isPositiveSafeInteger(value) {
|
|
10
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
11
|
+
}
|
|
12
|
+
/** Check data received from a host before using it as a paste-token block. */
|
|
13
|
+
function isPasteBlock(value) {
|
|
14
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
15
|
+
const candidate = value;
|
|
16
|
+
return typeof candidate.id === "string" && PASTE_ID_PATTERN.test(candidate.id) && isPositiveSafeInteger(candidate.seq) && isPositiveSafeInteger(candidate.lines) && typeof candidate.content === "string";
|
|
17
|
+
}
|
|
18
|
+
function unambiguousBlocks(blocks) {
|
|
19
|
+
const valid = blocks.filter(isPasteBlock);
|
|
20
|
+
const seqCounts = /* @__PURE__ */ new Map();
|
|
21
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const block of valid) {
|
|
23
|
+
seqCounts.set(block.seq, (seqCounts.get(block.seq) ?? 0) + 1);
|
|
24
|
+
idCounts.set(block.id, (idCounts.get(block.id) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
return valid.filter((block) => seqCounts.get(block.seq) === 1 && idCounts.get(block.id) === 1);
|
|
27
|
+
}
|
|
28
|
+
function formatToken(block) {
|
|
29
|
+
if (!isPasteBlock(block)) throw new TypeError("Cannot format an invalid paste block");
|
|
30
|
+
return `[ Paste #${block.seq} · ${block.lines} lines ]`;
|
|
31
|
+
}
|
|
32
|
+
function shouldCollapse(text) {
|
|
33
|
+
if (!text) return false;
|
|
34
|
+
return countLines(text) >= 3 || text.length >= 200;
|
|
35
|
+
}
|
|
36
|
+
function countLines(text) {
|
|
37
|
+
if (!text) return 0;
|
|
38
|
+
return text.split("\n").length;
|
|
39
|
+
}
|
|
40
|
+
/** Next sequence for a new paste = max existing + 1, starting at 1. */
|
|
41
|
+
function nextSeq(blocks) {
|
|
42
|
+
let max = 0;
|
|
43
|
+
for (const block of blocks) if (isPasteBlock(block) && block.seq > max) max = block.seq;
|
|
44
|
+
if (max === Number.MAX_SAFE_INTEGER) throw new RangeError("Paste sequence space is exhausted");
|
|
45
|
+
return max + 1;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Re-sequence `carried` blocks whose sequence is already taken by `used`, and
|
|
49
|
+
* rewrite their markers in one right-to-left pass. Carried block IDs and
|
|
50
|
+
* sequences must be unique so a visible marker always has one backing block.
|
|
51
|
+
* `used` is mutated to include the assigned sequences.
|
|
52
|
+
*/
|
|
53
|
+
function remapCarriedBlocks(text, carried, used) {
|
|
54
|
+
const seenSeqs = /* @__PURE__ */ new Set();
|
|
55
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
56
|
+
for (const block of carried) {
|
|
57
|
+
if (!isPasteBlock(block)) throw new TypeError("Cannot remap an invalid paste block");
|
|
58
|
+
if (seenSeqs.has(block.seq) || seenIds.has(block.id)) throw new TypeError("Carried paste block IDs and sequences must be unique");
|
|
59
|
+
seenSeqs.add(block.seq);
|
|
60
|
+
seenIds.add(block.id);
|
|
61
|
+
}
|
|
62
|
+
let maxUsed = 0;
|
|
63
|
+
for (const value of used) if (isPositiveSafeInteger(value) && value > maxUsed) maxUsed = value;
|
|
64
|
+
let free = maxUsed < Number.MAX_SAFE_INTEGER ? maxUsed + 1 : 1;
|
|
65
|
+
const allocateFree = () => {
|
|
66
|
+
while (used.has(free)) free = free < Number.MAX_SAFE_INTEGER ? free + 1 : 1;
|
|
67
|
+
const assigned = free;
|
|
68
|
+
used.add(assigned);
|
|
69
|
+
free = assigned < Number.MAX_SAFE_INTEGER ? assigned + 1 : 1;
|
|
70
|
+
return assigned;
|
|
71
|
+
};
|
|
72
|
+
const remap = /* @__PURE__ */ new Map();
|
|
73
|
+
const blocks = carried.map((block) => {
|
|
74
|
+
if (!used.has(block.seq)) {
|
|
75
|
+
used.add(block.seq);
|
|
76
|
+
return block;
|
|
77
|
+
}
|
|
78
|
+
const seq = allocateFree();
|
|
79
|
+
remap.set(block.seq, seq);
|
|
80
|
+
return {
|
|
81
|
+
...block,
|
|
82
|
+
seq
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
if (!remap.size) return {
|
|
86
|
+
text,
|
|
87
|
+
blocks
|
|
88
|
+
};
|
|
89
|
+
let out = text;
|
|
90
|
+
const ranges = findTokenRanges(text, carried);
|
|
91
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
92
|
+
const { start, end, block } = ranges[index];
|
|
93
|
+
const mapped = remap.get(block.seq);
|
|
94
|
+
if (mapped === void 0) continue;
|
|
95
|
+
out = out.slice(0, start) + formatToken({
|
|
96
|
+
...block,
|
|
97
|
+
seq: mapped
|
|
98
|
+
}) + out.slice(end);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
text: out,
|
|
102
|
+
blocks
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Ranges for canonical tokens whose sequence and line count match one block. */
|
|
106
|
+
function findTokenRanges(text, blocks) {
|
|
107
|
+
if (!text || !blocks.length) return [];
|
|
108
|
+
const bySeq = new Map(unambiguousBlocks(blocks).map((block) => [block.seq, block]));
|
|
109
|
+
if (!bySeq.size) return [];
|
|
110
|
+
const tokenRegex = new RegExp(PASTE_TOKEN_SOURCE, "g");
|
|
111
|
+
const ranges = [];
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
114
|
+
const seq = Number(match[1]);
|
|
115
|
+
const lines = Number(match[2]);
|
|
116
|
+
if (!isPositiveSafeInteger(seq) || !isPositiveSafeInteger(lines)) continue;
|
|
117
|
+
const block = bySeq.get(seq);
|
|
118
|
+
if (block && block.lines === lines) ranges.push({
|
|
119
|
+
start: match.index,
|
|
120
|
+
end: match.index + match[0].length,
|
|
121
|
+
block
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return ranges;
|
|
125
|
+
}
|
|
126
|
+
function tokenRangeAt(text, blocks, caret) {
|
|
127
|
+
if (!Number.isSafeInteger(caret) || caret < 0 || caret > text.length) return null;
|
|
128
|
+
for (const range of findTokenRanges(text, blocks)) if (caret >= range.start && caret <= range.end) return range;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function pruneBlocks(text, blocks) {
|
|
132
|
+
if (!blocks.length) return blocks;
|
|
133
|
+
const usable = unambiguousBlocks(blocks);
|
|
134
|
+
const survivors = new Set(findTokenRanges(text, usable).map((range) => range.block.id));
|
|
135
|
+
const next = usable.filter((block) => survivors.has(block.id));
|
|
136
|
+
return next.length === blocks.length && next.every((block, index) => block === blocks[index]) ? blocks : next;
|
|
137
|
+
}
|
|
138
|
+
function expandAll(text, blocks) {
|
|
139
|
+
if (!text || !blocks.length) return text;
|
|
140
|
+
const ranges = findTokenRanges(text, blocks);
|
|
141
|
+
if (!ranges.length) return text;
|
|
142
|
+
let out = text;
|
|
143
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const range = ranges[index];
|
|
145
|
+
out = out.slice(0, range.start) + range.block.content + out.slice(range.end);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Inverse of `expandAll`: replace each first non-overlapping verbatim block. */
|
|
150
|
+
function recollapsePastes(content, blocks) {
|
|
151
|
+
if (!content || !blocks.length) return content;
|
|
152
|
+
const hits = [];
|
|
153
|
+
const claimed = [];
|
|
154
|
+
const firstUnclaimed = (needle) => {
|
|
155
|
+
if (!needle) return -1;
|
|
156
|
+
let from = 0;
|
|
157
|
+
while (from <= content.length) {
|
|
158
|
+
const index = content.indexOf(needle, from);
|
|
159
|
+
if (index < 0) return -1;
|
|
160
|
+
if (!claimed.some(([start, end]) => index < end && index + needle.length > start)) return index;
|
|
161
|
+
from = index + 1;
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
};
|
|
165
|
+
for (const block of unambiguousBlocks(blocks)) {
|
|
166
|
+
if (!block.content) continue;
|
|
167
|
+
const trimmed = block.content.trimEnd();
|
|
168
|
+
let needle = block.content;
|
|
169
|
+
let index = firstUnclaimed(needle);
|
|
170
|
+
if (index < 0 && trimmed && trimmed !== block.content) {
|
|
171
|
+
needle = trimmed;
|
|
172
|
+
index = firstUnclaimed(needle);
|
|
173
|
+
}
|
|
174
|
+
if (index < 0) continue;
|
|
175
|
+
const end = index + needle.length;
|
|
176
|
+
hits.push({
|
|
177
|
+
start: index,
|
|
178
|
+
end,
|
|
179
|
+
block
|
|
180
|
+
});
|
|
181
|
+
claimed.push([index, end]);
|
|
182
|
+
}
|
|
183
|
+
if (!hits.length) return content;
|
|
184
|
+
hits.sort((left, right) => left.start - right.start);
|
|
185
|
+
let out = "";
|
|
186
|
+
let position = 0;
|
|
187
|
+
for (const hit of hits) {
|
|
188
|
+
if (hit.start < position) continue;
|
|
189
|
+
out += content.slice(position, hit.start) + formatToken(hit.block);
|
|
190
|
+
position = hit.end;
|
|
191
|
+
}
|
|
192
|
+
return out + content.slice(position);
|
|
193
|
+
}
|
|
194
|
+
/** Strip a trailing whitespace run only when that run contains a newline. */
|
|
195
|
+
function stripTrailingBlankLines(value) {
|
|
196
|
+
let index = value.length - 1;
|
|
197
|
+
let sawNewline = false;
|
|
198
|
+
while (index >= 0) {
|
|
199
|
+
const code = value.charCodeAt(index);
|
|
200
|
+
if (code === 10 || code === 13) {
|
|
201
|
+
sawNewline = true;
|
|
202
|
+
index -= 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (code === 32 || code === 9) {
|
|
206
|
+
index -= 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return sawNewline ? value.slice(0, index + 1) : value;
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
export { PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, countLines, expandAll, findTokenRanges, formatToken, isPasteBlock, nextSeq, pruneBlocks, recollapsePastes, remapCarriedBlocks, shouldCollapse, stripTrailingBlankLines, tokenRangeAt };
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=paste-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paste-tokens.js","names":[],"sources":["../../../src/components/conversation/paste-tokens.ts"],"sourcesContent":["/**\n * Pure paste-token transformations translated and hardened from KiroCrew's\n * `website/src/utils/pasteTokens.ts` at revision\n * `283e136c0f902e965a535a7c9548c57c7504fed0` (Apache-2.0). See the #532 entry\n * in the repository NOTICE. Paste identity is supplied by the host; this module\n * does not access storage, clipboard APIs, message services, or editor state.\n */\n\n/** A collapsed paste block stored alongside input or a message. */\nexport interface PasteBlock {\n /** A stable host-provided ASCII identifier; never included in visible token text. */\n id: string\n /** Positive safe integer, unique among blocks in the same text value. */\n seq: number\n /** Positive safe integer displayed in the token. */\n lines: number\n /** Original pasted text, retained verbatim. */\n content: string\n}\n\nexport const PASTE_THRESHOLD_LINES = 3\nexport const PASTE_THRESHOLD_CHARS = 200\n\nconst PASTE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/\nconst MAX_SAFE_SEQUENCE_TEXT_LENGTH = String(Number.MAX_SAFE_INTEGER).length\nconst PASTE_TOKEN_SOURCE = String.raw`\\[ Paste #([1-9]\\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) · ([1-9]\\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) lines \\]`\n\n/** Canonical token pattern. Internal scans always use a fresh RegExp instance. */\nexport const PASTE_TOKEN_REGEX = new RegExp(PASTE_TOKEN_SOURCE, 'g')\n\nfunction isPositiveSafeInteger(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) > 0\n}\n\n/** Check data received from a host before using it as a paste-token block. */\nexport function isPasteBlock(value: unknown): value is PasteBlock {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.id === 'string' &&\n PASTE_ID_PATTERN.test(candidate.id) &&\n isPositiveSafeInteger(candidate.seq) &&\n isPositiveSafeInteger(candidate.lines) &&\n typeof candidate.content === 'string'\n )\n}\n\nfunction unambiguousBlocks(blocks: readonly PasteBlock[]): PasteBlock[] {\n const valid = blocks.filter(isPasteBlock)\n const seqCounts = new Map<number, number>()\n const idCounts = new Map<string, number>()\n for (const block of valid) {\n seqCounts.set(block.seq, (seqCounts.get(block.seq) ?? 0) + 1)\n idCounts.set(block.id, (idCounts.get(block.id) ?? 0) + 1)\n }\n return valid.filter((block) => seqCounts.get(block.seq) === 1 && idCounts.get(block.id) === 1)\n}\n\nexport function formatToken(block: PasteBlock): string {\n if (!isPasteBlock(block)) throw new TypeError('Cannot format an invalid paste block')\n return `[ Paste #${block.seq} · ${block.lines} lines ]`\n}\n\nexport function shouldCollapse(text: string): boolean {\n if (!text) return false\n return countLines(text) >= PASTE_THRESHOLD_LINES || text.length >= PASTE_THRESHOLD_CHARS\n}\n\nexport function countLines(text: string): number {\n if (!text) return 0\n return text.split('\\n').length\n}\n\n/** Next sequence for a new paste = max existing + 1, starting at 1. */\nexport function nextSeq(blocks: readonly PasteBlock[]): number {\n let max = 0\n for (const block of blocks) {\n if (isPasteBlock(block) && block.seq > max) max = block.seq\n }\n if (max === Number.MAX_SAFE_INTEGER) throw new RangeError('Paste sequence space is exhausted')\n return max + 1\n}\n\n/**\n * Re-sequence `carried` blocks whose sequence is already taken by `used`, and\n * rewrite their markers in one right-to-left pass. Carried block IDs and\n * sequences must be unique so a visible marker always has one backing block.\n * `used` is mutated to include the assigned sequences.\n */\nexport function remapCarriedBlocks(\n text: string,\n carried: readonly PasteBlock[],\n used: Set<number>\n): { text: string; blocks: PasteBlock[] } {\n const seenSeqs = new Set<number>()\n const seenIds = new Set<string>()\n for (const block of carried) {\n if (!isPasteBlock(block)) throw new TypeError('Cannot remap an invalid paste block')\n if (seenSeqs.has(block.seq) || seenIds.has(block.id)) {\n throw new TypeError('Carried paste block IDs and sequences must be unique')\n }\n seenSeqs.add(block.seq)\n seenIds.add(block.id)\n }\n\n let maxUsed = 0\n for (const value of used) {\n if (isPositiveSafeInteger(value) && value > maxUsed) maxUsed = value\n }\n let free = maxUsed < Number.MAX_SAFE_INTEGER ? maxUsed + 1 : 1\n const allocateFree = (): number => {\n while (used.has(free)) {\n free = free < Number.MAX_SAFE_INTEGER ? free + 1 : 1\n }\n const assigned = free\n used.add(assigned)\n free = assigned < Number.MAX_SAFE_INTEGER ? assigned + 1 : 1\n return assigned\n }\n\n const remap = new Map<number, number>()\n const blocks = carried.map((block) => {\n if (!used.has(block.seq)) {\n used.add(block.seq)\n return block\n }\n const seq = allocateFree()\n remap.set(block.seq, seq)\n return { ...block, seq }\n })\n\n if (!remap.size) return { text, blocks }\n\n let out = text\n const ranges = findTokenRanges(text, carried)\n for (let index = ranges.length - 1; index >= 0; index -= 1) {\n const { start, end, block } = ranges[index]!\n const mapped = remap.get(block.seq)\n if (mapped === undefined) continue\n out = out.slice(0, start) + formatToken({ ...block, seq: mapped }) + out.slice(end)\n }\n return { text: out, blocks }\n}\n\n/** Ranges for canonical tokens whose sequence and line count match one block. */\nexport function findTokenRanges(\n text: string,\n blocks: readonly PasteBlock[]\n): Array<{ start: number; end: number; block: PasteBlock }> {\n if (!text || !blocks.length) return []\n const bySeq = new Map(unambiguousBlocks(blocks).map((block) => [block.seq, block]))\n if (!bySeq.size) return []\n\n const tokenRegex = new RegExp(PASTE_TOKEN_SOURCE, 'g')\n const ranges: Array<{ start: number; end: number; block: PasteBlock }> = []\n let match: RegExpExecArray | null\n while ((match = tokenRegex.exec(text)) !== null) {\n const seq = Number(match[1])\n const lines = Number(match[2])\n if (!isPositiveSafeInteger(seq) || !isPositiveSafeInteger(lines)) continue\n const block = bySeq.get(seq)\n if (block && block.lines === lines) {\n ranges.push({ start: match.index, end: match.index + match[0].length, block })\n }\n }\n return ranges\n}\n\nexport function tokenRangeAt(\n text: string,\n blocks: readonly PasteBlock[],\n caret: number\n): { start: number; end: number; block: PasteBlock } | null {\n if (!Number.isSafeInteger(caret) || caret < 0 || caret > text.length) return null\n for (const range of findTokenRanges(text, blocks)) {\n if (caret >= range.start && caret <= range.end) return range\n }\n return null\n}\n\nexport function pruneBlocks(text: string, blocks: PasteBlock[]): PasteBlock[] {\n if (!blocks.length) return blocks\n const usable = unambiguousBlocks(blocks)\n const survivors = new Set(findTokenRanges(text, usable).map((range) => range.block.id))\n const next = usable.filter((block) => survivors.has(block.id))\n return next.length === blocks.length && next.every((block, index) => block === blocks[index])\n ? blocks\n : next\n}\n\nexport function expandAll(text: string, blocks: readonly PasteBlock[]): string {\n if (!text || !blocks.length) return text\n const ranges = findTokenRanges(text, blocks)\n if (!ranges.length) return text\n let out = text\n for (let index = ranges.length - 1; index >= 0; index -= 1) {\n const range = ranges[index]!\n out = out.slice(0, range.start) + range.block.content + out.slice(range.end)\n }\n return out\n}\n\n/** Inverse of `expandAll`: replace each first non-overlapping verbatim block. */\nexport function recollapsePastes(content: string, blocks: readonly PasteBlock[]): string {\n if (!content || !blocks.length) return content\n\n interface Hit {\n start: number\n end: number\n block: PasteBlock\n }\n const hits: Hit[] = []\n const claimed: Array<[number, number]> = []\n const firstUnclaimed = (needle: string): number => {\n if (!needle) return -1\n let from = 0\n while (from <= content.length) {\n const index = content.indexOf(needle, from)\n if (index < 0) return -1\n if (!claimed.some(([start, end]) => index < end && index + needle.length > start))\n return index\n from = index + 1\n }\n return -1\n }\n\n for (const block of unambiguousBlocks(blocks)) {\n if (!block.content) continue\n const trimmed = block.content.trimEnd()\n let needle = block.content\n let index = firstUnclaimed(needle)\n if (index < 0 && trimmed && trimmed !== block.content) {\n needle = trimmed\n index = firstUnclaimed(needle)\n }\n if (index < 0) continue\n const end = index + needle.length\n hits.push({ start: index, end, block })\n claimed.push([index, end])\n }\n\n if (!hits.length) return content\n hits.sort((left, right) => left.start - right.start)\n let out = ''\n let position = 0\n for (const hit of hits) {\n if (hit.start < position) continue\n out += content.slice(position, hit.start) + formatToken(hit.block)\n position = hit.end\n }\n return out + content.slice(position)\n}\n\n/** Strip a trailing whitespace run only when that run contains a newline. */\nexport function stripTrailingBlankLines(value: string): string {\n let index = value.length - 1\n let sawNewline = false\n while (index >= 0) {\n const code = value.charCodeAt(index)\n if (code === 10 || code === 13) {\n sawNewline = true\n index -= 1\n continue\n }\n if (code === 32 || code === 9) {\n index -= 1\n continue\n }\n break\n }\n return sawNewline ? value.slice(0, index + 1) : value\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,7 @@ import { ConversationSurface } from "./components/conversation/conversation-surf
|
|
|
77
77
|
import { ComposerAttachmentButton, ComposerHint, MessageComposer } from "./components/conversation/message-composer.js";
|
|
78
78
|
import { MessageBody, MessageDayDivider, MessageGroup, MessageRow } from "./components/conversation/message-row.js";
|
|
79
79
|
import { AttachmentCard, ThreadPanel } from "./components/conversation/thread-panel.js";
|
|
80
|
+
import { PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, countLines, expandAll, findTokenRanges, formatToken, isPasteBlock, nextSeq, pruneBlocks, recollapsePastes, remapCarriedBlocks, shouldCollapse, stripTrailingBlankLines, tokenRangeAt } from "./components/conversation/paste-tokens.js";
|
|
80
81
|
import { accentPresets, allTokens, colorTokens, densityTokens, designTokens, elevationTokens, fontOptions, motionTokens, radiusTokens, typographyTokens, undocumentedTokenAliases, zIndexTokens } from "./lib/tokens.js";
|
|
81
82
|
import { builtinThemes, contrastRatio, defaultDarkThemeId, defaultLightThemeId, themeById, themeCssVariables, themeFamilies, themesForAppearance, validateTheme, validateThemeRegistry } from "./lib/themes.js";
|
|
82
83
|
import { ThemePicker, ThemePreview, ThemeSwatch } from "./components/theme/theme-picker.js";
|
|
@@ -93,4 +94,4 @@ import { formatBytes, formatReleaseDate, plainTextFromMarkdown } from "./lib/ver
|
|
|
93
94
|
import { UpdateDialog } from "./components/composites/update-dialog/update-dialog.js";
|
|
94
95
|
import { WorkspaceMark } from "./components/composites/workspace-mark/workspace-mark.js";
|
|
95
96
|
import { keyedRows } from "./lib/keyed-rows.js";
|
|
96
|
-
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AccountMenu, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppShell, AppShellBody, AppShellContent, AppShellMain, AppearanceEditor, AppearancePanel, AppearancePopover, AspectRatio, AttachmentCard, Avatar, AvatarFallback, AvatarImage, Badge, Board, BoardCardBody, BoardCardTitle, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, BusySendButton, Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, CalendarSurface, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatComposer, Checkbox, CheckboxDescription, CheckboxLabel, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, Combobox, ComboboxClear, ComboboxContent, ComboboxControl, ComboboxInput, ComboboxItem, ComboboxSection, ComboboxTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading, CommandSeparator, CommandShortcut, ComposerAttachmentButton, ComposerHint, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, ConversationAvatar, ConversationSurface, DatePicker, DetailPanel, DetailPanelBody, DetailPanelField, DetailPanelHeader, DetailPanelSection, Dialog, DialogClose, DialogCloseButton, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffBlock, DiffSummary, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, EntityIcon, Field, FieldDescription, FieldError, FieldInput, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTextArea, HoverCard, HoverCardContent, HoverCardTrigger, InlineCode, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputOtp, InputOtpCaret, Item, ItemDescription, ItemEmpty, ItemGroup, ItemGroupEntry, ItemTitle, Kbd, KbdGroup, Label, ListGroup, ListRow, MAX_LAYOUT_DEPTH, MAX_LAYOUT_LEAVES, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, MessageBody, MessageComposer, MessageDayDivider, MessageGroup, MessageRow, ModalDialog, Motion, NavigationMenu, NavigationMenuContent, NavigationMenuGroup, NavigationMenuGroupLabel, NavigationMenuItem, NavigationMenuMenu, NavigationMenuSeparator, NavigationMenuTrigger, NavigationMenuViewport, Page, PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderTitle, PageSection, Pagination, PaginationEllipsis, PaginationItem, PaginationNext, PaginationPrevious, PalettePreview, Panel, PanelActions, PanelBody, PanelCard, PanelDescription, PanelFooter, PanelHeader, PanelPlaceholder, PanelTitle, PanelToolbar, Popover, PopoverAnchor, PopoverCloseButton, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger, Presence, Progress, PropertyList, PropertyRow, PropertyTerm, PropertyValue, RadioGroup, RadioGroupItem, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, Select, SelectChevronsUpDown, SelectContent, SelectItem, SelectLabel, SelectSection, SelectTrigger, SelectValue, Separator, SettingsField, SettingsPage, SettingsRow, SettingsSection, Sheet, SheetBody, SheetCloseButton, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SideRail, SideRailButton, SideRailContent, SideRailFooter, SideRailHeader, SideRailItem, SideRailSection, SidebarNav, SidebarNavButton, SidebarNavContent, SidebarNavFooter, SidebarNavHeader, SidebarNavItem, SidebarNavSection, SidebarNavTitle, Skeleton, Slider, Spinner, SplitLayout, Stat, StatGroup, StatusBar, StatusBarItem, StatusBarSpacer, StatusChip, StatusList, Switch, SwitchDescription, SwitchLabel, TALL_CODE_BLOCK_PX, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableNumericCell, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ThemeMiniature, ThemeMiniatureSplit, ThemePicker, ThemePreview, ThemeProvider, ThemeSwatch, ThemeToggle, ThreadPanel, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TopBar, TopBarBreadcrumb, TopBarPill, TopBarSearch, TopBarSection, TopBarTitle, UpdateDialog, WorkspaceMark, accentPresets, alertVariants, allTokens, badgeVariants, boardCardTitleVariants, builtinThemes, buttonVariants, closePane, cn, colorTokens, computeLayoutFrames, contrastRatio, controlInteractive, controlSize, controlSizeIcon, controlSizes, countLeaves, createLayoutState, cva, defaultDarkThemeId, defaultLightThemeId, defaultThemeSelection, densityTokens, designTokens, diffLineVariants, diffStats, diffTargetPath, elevationTokens, entityIconVariants, focusPane, fontOptions, formatBytes, formatReleaseDate, initialsFrom, inlineScriptLiteral, itemVariants, keyedRows, layoutDepth, listLeaves, monogram, motionTokens, movePane, neighborLeaf, normalizeLayout, parseDiff, plainTextFromMarkdown, radiusTokens, resizeSplit, resolvedDensity, sheetVariants, sidebarNavItemClass, sidebarNavItemStateClass, splitPane, statusDotVariants, surfaceInteractive, swapPanes, themeById, themeCssVariables, themeFamilies, themeScript, themesForAppearance, toast, toaster, toggleVariants, typographyTokens, undoClosePane, undocumentedTokenAliases, useSideRail, useTheme, validateTheme, validateThemeRegistry, zIndexTokens };
|
|
97
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AccountMenu, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppShell, AppShellBody, AppShellContent, AppShellMain, AppearanceEditor, AppearancePanel, AppearancePopover, AspectRatio, AttachmentCard, Avatar, AvatarFallback, AvatarImage, Badge, Board, BoardCardBody, BoardCardTitle, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, BusySendButton, Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, CalendarSurface, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatComposer, Checkbox, CheckboxDescription, CheckboxLabel, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, Combobox, ComboboxClear, ComboboxContent, ComboboxControl, ComboboxInput, ComboboxItem, ComboboxSection, ComboboxTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading, CommandSeparator, CommandShortcut, ComposerAttachmentButton, ComposerHint, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, ConversationAvatar, ConversationSurface, DatePicker, DetailPanel, DetailPanelBody, DetailPanelField, DetailPanelHeader, DetailPanelSection, Dialog, DialogClose, DialogCloseButton, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffBlock, DiffSummary, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, EntityIcon, Field, FieldDescription, FieldError, FieldInput, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTextArea, HoverCard, HoverCardContent, HoverCardTrigger, InlineCode, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputOtp, InputOtpCaret, Item, ItemDescription, ItemEmpty, ItemGroup, ItemGroupEntry, ItemTitle, Kbd, KbdGroup, Label, ListGroup, ListRow, MAX_LAYOUT_DEPTH, MAX_LAYOUT_LEAVES, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, MessageBody, MessageComposer, MessageDayDivider, MessageGroup, MessageRow, ModalDialog, Motion, NavigationMenu, NavigationMenuContent, NavigationMenuGroup, NavigationMenuGroupLabel, NavigationMenuItem, NavigationMenuMenu, NavigationMenuSeparator, NavigationMenuTrigger, NavigationMenuViewport, PASTE_THRESHOLD_CHARS, PASTE_THRESHOLD_LINES, PASTE_TOKEN_REGEX, Page, PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderTitle, PageSection, Pagination, PaginationEllipsis, PaginationItem, PaginationNext, PaginationPrevious, PalettePreview, Panel, PanelActions, PanelBody, PanelCard, PanelDescription, PanelFooter, PanelHeader, PanelPlaceholder, PanelTitle, PanelToolbar, Popover, PopoverAnchor, PopoverCloseButton, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger, Presence, Progress, PropertyList, PropertyRow, PropertyTerm, PropertyValue, RadioGroup, RadioGroupItem, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, Select, SelectChevronsUpDown, SelectContent, SelectItem, SelectLabel, SelectSection, SelectTrigger, SelectValue, Separator, SettingsField, SettingsPage, SettingsRow, SettingsSection, Sheet, SheetBody, SheetCloseButton, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, SideRail, SideRailButton, SideRailContent, SideRailFooter, SideRailHeader, SideRailItem, SideRailSection, SidebarNav, SidebarNavButton, SidebarNavContent, SidebarNavFooter, SidebarNavHeader, SidebarNavItem, SidebarNavSection, SidebarNavTitle, Skeleton, Slider, Spinner, SplitLayout, Stat, StatGroup, StatusBar, StatusBarItem, StatusBarSpacer, StatusChip, StatusList, Switch, SwitchDescription, SwitchLabel, TALL_CODE_BLOCK_PX, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableNumericCell, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ThemeMiniature, ThemeMiniatureSplit, ThemePicker, ThemePreview, ThemeProvider, ThemeSwatch, ThemeToggle, ThreadPanel, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TopBar, TopBarBreadcrumb, TopBarPill, TopBarSearch, TopBarSection, TopBarTitle, UpdateDialog, WorkspaceMark, accentPresets, alertVariants, allTokens, badgeVariants, boardCardTitleVariants, builtinThemes, buttonVariants, closePane, cn, colorTokens, computeLayoutFrames, contrastRatio, controlInteractive, controlSize, controlSizeIcon, controlSizes, countLeaves, countLines, createLayoutState, cva, defaultDarkThemeId, defaultLightThemeId, defaultThemeSelection, densityTokens, designTokens, diffLineVariants, diffStats, diffTargetPath, elevationTokens, entityIconVariants, expandAll, findTokenRanges, focusPane, fontOptions, formatBytes, formatReleaseDate, formatToken, initialsFrom, inlineScriptLiteral, isPasteBlock, itemVariants, keyedRows, layoutDepth, listLeaves, monogram, motionTokens, movePane, neighborLeaf, nextSeq, normalizeLayout, parseDiff, plainTextFromMarkdown, pruneBlocks, radiusTokens, recollapsePastes, remapCarriedBlocks, resizeSplit, resolvedDensity, sheetVariants, shouldCollapse, sidebarNavItemClass, sidebarNavItemStateClass, splitPane, statusDotVariants, stripTrailingBlankLines, surfaceInteractive, swapPanes, themeById, themeCssVariables, themeFamilies, themeScript, themesForAppearance, toast, toaster, toggleVariants, tokenRangeAt, typographyTokens, undoClosePane, undocumentedTokenAliases, useSideRail, useTheme, validateTheme, validateThemeRegistry, zIndexTokens };
|
package/dist/r/conversation.json
CHANGED
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"type": "registry:ui",
|
|
58
58
|
"target": "components/conversation/message-row.tsx"
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"path": "src/components/conversation/paste-tokens.ts",
|
|
62
|
+
"type": "registry:ui",
|
|
63
|
+
"target": "components/conversation/paste-tokens.ts"
|
|
64
|
+
},
|
|
60
65
|
{
|
|
61
66
|
"path": "src/components/conversation/scroll-follow-core.ts",
|
|
62
67
|
"type": "registry:ui",
|
package/dist/r/registry.json
CHANGED
|
@@ -1871,6 +1871,11 @@
|
|
|
1871
1871
|
"type": "registry:ui",
|
|
1872
1872
|
"target": "components/conversation/message-row.tsx"
|
|
1873
1873
|
},
|
|
1874
|
+
{
|
|
1875
|
+
"path": "src/components/conversation/paste-tokens.ts",
|
|
1876
|
+
"type": "registry:ui",
|
|
1877
|
+
"target": "components/conversation/paste-tokens.ts"
|
|
1878
|
+
},
|
|
1874
1879
|
{
|
|
1875
1880
|
"path": "src/components/conversation/scroll-follow-core.ts",
|
|
1876
1881
|
"type": "registry:ui",
|
|
@@ -25,3 +25,22 @@ export {
|
|
|
25
25
|
type AttachmentCardProps,
|
|
26
26
|
type ThreadPanelProps,
|
|
27
27
|
} from './thread-panel'
|
|
28
|
+
// Issue #532 selected pure paste-token model.
|
|
29
|
+
export {
|
|
30
|
+
countLines,
|
|
31
|
+
expandAll,
|
|
32
|
+
findTokenRanges,
|
|
33
|
+
formatToken,
|
|
34
|
+
isPasteBlock,
|
|
35
|
+
nextSeq,
|
|
36
|
+
pruneBlocks,
|
|
37
|
+
recollapsePastes,
|
|
38
|
+
remapCarriedBlocks,
|
|
39
|
+
shouldCollapse,
|
|
40
|
+
stripTrailingBlankLines,
|
|
41
|
+
tokenRangeAt,
|
|
42
|
+
PASTE_THRESHOLD_CHARS,
|
|
43
|
+
PASTE_THRESHOLD_LINES,
|
|
44
|
+
PASTE_TOKEN_REGEX,
|
|
45
|
+
type PasteBlock,
|
|
46
|
+
} from './paste-tokens'
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure paste-token transformations translated and hardened from KiroCrew's
|
|
3
|
+
* `website/src/utils/pasteTokens.ts` at revision
|
|
4
|
+
* `283e136c0f902e965a535a7c9548c57c7504fed0` (Apache-2.0). See the #532 entry
|
|
5
|
+
* in the repository NOTICE. Paste identity is supplied by the host; this module
|
|
6
|
+
* does not access storage, clipboard APIs, message services, or editor state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A collapsed paste block stored alongside input or a message. */
|
|
10
|
+
export interface PasteBlock {
|
|
11
|
+
/** A stable host-provided ASCII identifier; never included in visible token text. */
|
|
12
|
+
id: string
|
|
13
|
+
/** Positive safe integer, unique among blocks in the same text value. */
|
|
14
|
+
seq: number
|
|
15
|
+
/** Positive safe integer displayed in the token. */
|
|
16
|
+
lines: number
|
|
17
|
+
/** Original pasted text, retained verbatim. */
|
|
18
|
+
content: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const PASTE_THRESHOLD_LINES = 3
|
|
22
|
+
export const PASTE_THRESHOLD_CHARS = 200
|
|
23
|
+
|
|
24
|
+
const PASTE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
|
|
25
|
+
const MAX_SAFE_SEQUENCE_TEXT_LENGTH = String(Number.MAX_SAFE_INTEGER).length
|
|
26
|
+
const PASTE_TOKEN_SOURCE = String.raw`\[ Paste #([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) · ([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) lines \]`
|
|
27
|
+
|
|
28
|
+
/** Canonical token pattern. Internal scans always use a fresh RegExp instance. */
|
|
29
|
+
export const PASTE_TOKEN_REGEX = new RegExp(PASTE_TOKEN_SOURCE, 'g')
|
|
30
|
+
|
|
31
|
+
function isPositiveSafeInteger(value: unknown): value is number {
|
|
32
|
+
return Number.isSafeInteger(value) && (value as number) > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Check data received from a host before using it as a paste-token block. */
|
|
36
|
+
export function isPasteBlock(value: unknown): value is PasteBlock {
|
|
37
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
38
|
+
const candidate = value as Record<string, unknown>
|
|
39
|
+
return (
|
|
40
|
+
typeof candidate.id === 'string' &&
|
|
41
|
+
PASTE_ID_PATTERN.test(candidate.id) &&
|
|
42
|
+
isPositiveSafeInteger(candidate.seq) &&
|
|
43
|
+
isPositiveSafeInteger(candidate.lines) &&
|
|
44
|
+
typeof candidate.content === 'string'
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function unambiguousBlocks(blocks: readonly PasteBlock[]): PasteBlock[] {
|
|
49
|
+
const valid = blocks.filter(isPasteBlock)
|
|
50
|
+
const seqCounts = new Map<number, number>()
|
|
51
|
+
const idCounts = new Map<string, number>()
|
|
52
|
+
for (const block of valid) {
|
|
53
|
+
seqCounts.set(block.seq, (seqCounts.get(block.seq) ?? 0) + 1)
|
|
54
|
+
idCounts.set(block.id, (idCounts.get(block.id) ?? 0) + 1)
|
|
55
|
+
}
|
|
56
|
+
return valid.filter((block) => seqCounts.get(block.seq) === 1 && idCounts.get(block.id) === 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatToken(block: PasteBlock): string {
|
|
60
|
+
if (!isPasteBlock(block)) throw new TypeError('Cannot format an invalid paste block')
|
|
61
|
+
return `[ Paste #${block.seq} · ${block.lines} lines ]`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function shouldCollapse(text: string): boolean {
|
|
65
|
+
if (!text) return false
|
|
66
|
+
return countLines(text) >= PASTE_THRESHOLD_LINES || text.length >= PASTE_THRESHOLD_CHARS
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function countLines(text: string): number {
|
|
70
|
+
if (!text) return 0
|
|
71
|
+
return text.split('\n').length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Next sequence for a new paste = max existing + 1, starting at 1. */
|
|
75
|
+
export function nextSeq(blocks: readonly PasteBlock[]): number {
|
|
76
|
+
let max = 0
|
|
77
|
+
for (const block of blocks) {
|
|
78
|
+
if (isPasteBlock(block) && block.seq > max) max = block.seq
|
|
79
|
+
}
|
|
80
|
+
if (max === Number.MAX_SAFE_INTEGER) throw new RangeError('Paste sequence space is exhausted')
|
|
81
|
+
return max + 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Re-sequence `carried` blocks whose sequence is already taken by `used`, and
|
|
86
|
+
* rewrite their markers in one right-to-left pass. Carried block IDs and
|
|
87
|
+
* sequences must be unique so a visible marker always has one backing block.
|
|
88
|
+
* `used` is mutated to include the assigned sequences.
|
|
89
|
+
*/
|
|
90
|
+
export function remapCarriedBlocks(
|
|
91
|
+
text: string,
|
|
92
|
+
carried: readonly PasteBlock[],
|
|
93
|
+
used: Set<number>
|
|
94
|
+
): { text: string; blocks: PasteBlock[] } {
|
|
95
|
+
const seenSeqs = new Set<number>()
|
|
96
|
+
const seenIds = new Set<string>()
|
|
97
|
+
for (const block of carried) {
|
|
98
|
+
if (!isPasteBlock(block)) throw new TypeError('Cannot remap an invalid paste block')
|
|
99
|
+
if (seenSeqs.has(block.seq) || seenIds.has(block.id)) {
|
|
100
|
+
throw new TypeError('Carried paste block IDs and sequences must be unique')
|
|
101
|
+
}
|
|
102
|
+
seenSeqs.add(block.seq)
|
|
103
|
+
seenIds.add(block.id)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let maxUsed = 0
|
|
107
|
+
for (const value of used) {
|
|
108
|
+
if (isPositiveSafeInteger(value) && value > maxUsed) maxUsed = value
|
|
109
|
+
}
|
|
110
|
+
let free = maxUsed < Number.MAX_SAFE_INTEGER ? maxUsed + 1 : 1
|
|
111
|
+
const allocateFree = (): number => {
|
|
112
|
+
while (used.has(free)) {
|
|
113
|
+
free = free < Number.MAX_SAFE_INTEGER ? free + 1 : 1
|
|
114
|
+
}
|
|
115
|
+
const assigned = free
|
|
116
|
+
used.add(assigned)
|
|
117
|
+
free = assigned < Number.MAX_SAFE_INTEGER ? assigned + 1 : 1
|
|
118
|
+
return assigned
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const remap = new Map<number, number>()
|
|
122
|
+
const blocks = carried.map((block) => {
|
|
123
|
+
if (!used.has(block.seq)) {
|
|
124
|
+
used.add(block.seq)
|
|
125
|
+
return block
|
|
126
|
+
}
|
|
127
|
+
const seq = allocateFree()
|
|
128
|
+
remap.set(block.seq, seq)
|
|
129
|
+
return { ...block, seq }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (!remap.size) return { text, blocks }
|
|
133
|
+
|
|
134
|
+
let out = text
|
|
135
|
+
const ranges = findTokenRanges(text, carried)
|
|
136
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
137
|
+
const { start, end, block } = ranges[index]!
|
|
138
|
+
const mapped = remap.get(block.seq)
|
|
139
|
+
if (mapped === undefined) continue
|
|
140
|
+
out = out.slice(0, start) + formatToken({ ...block, seq: mapped }) + out.slice(end)
|
|
141
|
+
}
|
|
142
|
+
return { text: out, blocks }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Ranges for canonical tokens whose sequence and line count match one block. */
|
|
146
|
+
export function findTokenRanges(
|
|
147
|
+
text: string,
|
|
148
|
+
blocks: readonly PasteBlock[]
|
|
149
|
+
): Array<{ start: number; end: number; block: PasteBlock }> {
|
|
150
|
+
if (!text || !blocks.length) return []
|
|
151
|
+
const bySeq = new Map(unambiguousBlocks(blocks).map((block) => [block.seq, block]))
|
|
152
|
+
if (!bySeq.size) return []
|
|
153
|
+
|
|
154
|
+
const tokenRegex = new RegExp(PASTE_TOKEN_SOURCE, 'g')
|
|
155
|
+
const ranges: Array<{ start: number; end: number; block: PasteBlock }> = []
|
|
156
|
+
let match: RegExpExecArray | null
|
|
157
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
158
|
+
const seq = Number(match[1])
|
|
159
|
+
const lines = Number(match[2])
|
|
160
|
+
if (!isPositiveSafeInteger(seq) || !isPositiveSafeInteger(lines)) continue
|
|
161
|
+
const block = bySeq.get(seq)
|
|
162
|
+
if (block && block.lines === lines) {
|
|
163
|
+
ranges.push({ start: match.index, end: match.index + match[0].length, block })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return ranges
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function tokenRangeAt(
|
|
170
|
+
text: string,
|
|
171
|
+
blocks: readonly PasteBlock[],
|
|
172
|
+
caret: number
|
|
173
|
+
): { start: number; end: number; block: PasteBlock } | null {
|
|
174
|
+
if (!Number.isSafeInteger(caret) || caret < 0 || caret > text.length) return null
|
|
175
|
+
for (const range of findTokenRanges(text, blocks)) {
|
|
176
|
+
if (caret >= range.start && caret <= range.end) return range
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function pruneBlocks(text: string, blocks: PasteBlock[]): PasteBlock[] {
|
|
182
|
+
if (!blocks.length) return blocks
|
|
183
|
+
const usable = unambiguousBlocks(blocks)
|
|
184
|
+
const survivors = new Set(findTokenRanges(text, usable).map((range) => range.block.id))
|
|
185
|
+
const next = usable.filter((block) => survivors.has(block.id))
|
|
186
|
+
return next.length === blocks.length && next.every((block, index) => block === blocks[index])
|
|
187
|
+
? blocks
|
|
188
|
+
: next
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function expandAll(text: string, blocks: readonly PasteBlock[]): string {
|
|
192
|
+
if (!text || !blocks.length) return text
|
|
193
|
+
const ranges = findTokenRanges(text, blocks)
|
|
194
|
+
if (!ranges.length) return text
|
|
195
|
+
let out = text
|
|
196
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
197
|
+
const range = ranges[index]!
|
|
198
|
+
out = out.slice(0, range.start) + range.block.content + out.slice(range.end)
|
|
199
|
+
}
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Inverse of `expandAll`: replace each first non-overlapping verbatim block. */
|
|
204
|
+
export function recollapsePastes(content: string, blocks: readonly PasteBlock[]): string {
|
|
205
|
+
if (!content || !blocks.length) return content
|
|
206
|
+
|
|
207
|
+
interface Hit {
|
|
208
|
+
start: number
|
|
209
|
+
end: number
|
|
210
|
+
block: PasteBlock
|
|
211
|
+
}
|
|
212
|
+
const hits: Hit[] = []
|
|
213
|
+
const claimed: Array<[number, number]> = []
|
|
214
|
+
const firstUnclaimed = (needle: string): number => {
|
|
215
|
+
if (!needle) return -1
|
|
216
|
+
let from = 0
|
|
217
|
+
while (from <= content.length) {
|
|
218
|
+
const index = content.indexOf(needle, from)
|
|
219
|
+
if (index < 0) return -1
|
|
220
|
+
if (!claimed.some(([start, end]) => index < end && index + needle.length > start))
|
|
221
|
+
return index
|
|
222
|
+
from = index + 1
|
|
223
|
+
}
|
|
224
|
+
return -1
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const block of unambiguousBlocks(blocks)) {
|
|
228
|
+
if (!block.content) continue
|
|
229
|
+
const trimmed = block.content.trimEnd()
|
|
230
|
+
let needle = block.content
|
|
231
|
+
let index = firstUnclaimed(needle)
|
|
232
|
+
if (index < 0 && trimmed && trimmed !== block.content) {
|
|
233
|
+
needle = trimmed
|
|
234
|
+
index = firstUnclaimed(needle)
|
|
235
|
+
}
|
|
236
|
+
if (index < 0) continue
|
|
237
|
+
const end = index + needle.length
|
|
238
|
+
hits.push({ start: index, end, block })
|
|
239
|
+
claimed.push([index, end])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!hits.length) return content
|
|
243
|
+
hits.sort((left, right) => left.start - right.start)
|
|
244
|
+
let out = ''
|
|
245
|
+
let position = 0
|
|
246
|
+
for (const hit of hits) {
|
|
247
|
+
if (hit.start < position) continue
|
|
248
|
+
out += content.slice(position, hit.start) + formatToken(hit.block)
|
|
249
|
+
position = hit.end
|
|
250
|
+
}
|
|
251
|
+
return out + content.slice(position)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Strip a trailing whitespace run only when that run contains a newline. */
|
|
255
|
+
export function stripTrailingBlankLines(value: string): string {
|
|
256
|
+
let index = value.length - 1
|
|
257
|
+
let sawNewline = false
|
|
258
|
+
while (index >= 0) {
|
|
259
|
+
const code = value.charCodeAt(index)
|
|
260
|
+
if (code === 10 || code === 13) {
|
|
261
|
+
sawNewline = true
|
|
262
|
+
index -= 1
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (code === 32 || code === 9) {
|
|
266
|
+
index -= 1
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
return sawNewline ? value.slice(0, index + 1) : value
|
|
272
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adea",
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
"solid": "./src/components/layout/split-layout/model.ts",
|
|
71
71
|
"development": "./src/components/layout/split-layout/model.ts",
|
|
72
72
|
"import": "./dist/components/layout/split-layout/model.js"
|
|
73
|
+
},
|
|
74
|
+
"./components/conversation/paste-tokens": {
|
|
75
|
+
"types": "./dist/components/conversation/paste-tokens.d.ts",
|
|
76
|
+
"solid": "./src/components/conversation/paste-tokens.ts",
|
|
77
|
+
"development": "./src/components/conversation/paste-tokens.ts",
|
|
78
|
+
"import": "./dist/components/conversation/paste-tokens.js"
|
|
73
79
|
}
|
|
74
80
|
},
|
|
75
81
|
"scripts": {
|
|
@@ -88,7 +94,8 @@
|
|
|
88
94
|
"check:packed-consumer": "bun scripts/check-packed-consumer.ts",
|
|
89
95
|
"check:packed-conversation": "bun scripts/check-packed-conversation.ts",
|
|
90
96
|
"check:packed-layout-renderer": "bun scripts/check-packed-layout-renderer.ts",
|
|
91
|
-
"check:packed-appearance": "bun scripts/check-packed-appearance.ts"
|
|
97
|
+
"check:packed-appearance": "bun scripts/check-packed-appearance.ts",
|
|
98
|
+
"check:packed-paste-model": "node scripts/check-packed-paste-model.mjs"
|
|
92
99
|
},
|
|
93
100
|
"dependencies": {
|
|
94
101
|
"@adea-ai/themes": "^0.5.0",
|
package/registry.json
CHANGED
|
@@ -1871,6 +1871,11 @@
|
|
|
1871
1871
|
"type": "registry:ui",
|
|
1872
1872
|
"target": "components/conversation/message-row.tsx"
|
|
1873
1873
|
},
|
|
1874
|
+
{
|
|
1875
|
+
"path": "src/components/conversation/paste-tokens.ts",
|
|
1876
|
+
"type": "registry:ui",
|
|
1877
|
+
"target": "components/conversation/paste-tokens.ts"
|
|
1878
|
+
},
|
|
1874
1879
|
{
|
|
1875
1880
|
"path": "src/components/conversation/scroll-follow-core.ts",
|
|
1876
1881
|
"type": "registry:ui",
|
|
@@ -25,3 +25,22 @@ export {
|
|
|
25
25
|
type AttachmentCardProps,
|
|
26
26
|
type ThreadPanelProps,
|
|
27
27
|
} from './thread-panel'
|
|
28
|
+
// Issue #532 selected pure paste-token model.
|
|
29
|
+
export {
|
|
30
|
+
countLines,
|
|
31
|
+
expandAll,
|
|
32
|
+
findTokenRanges,
|
|
33
|
+
formatToken,
|
|
34
|
+
isPasteBlock,
|
|
35
|
+
nextSeq,
|
|
36
|
+
pruneBlocks,
|
|
37
|
+
recollapsePastes,
|
|
38
|
+
remapCarriedBlocks,
|
|
39
|
+
shouldCollapse,
|
|
40
|
+
stripTrailingBlankLines,
|
|
41
|
+
tokenRangeAt,
|
|
42
|
+
PASTE_THRESHOLD_CHARS,
|
|
43
|
+
PASTE_THRESHOLD_LINES,
|
|
44
|
+
PASTE_TOKEN_REGEX,
|
|
45
|
+
type PasteBlock,
|
|
46
|
+
} from './paste-tokens'
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure paste-token transformations translated and hardened from KiroCrew's
|
|
3
|
+
* `website/src/utils/pasteTokens.ts` at revision
|
|
4
|
+
* `283e136c0f902e965a535a7c9548c57c7504fed0` (Apache-2.0). See the #532 entry
|
|
5
|
+
* in the repository NOTICE. Paste identity is supplied by the host; this module
|
|
6
|
+
* does not access storage, clipboard APIs, message services, or editor state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A collapsed paste block stored alongside input or a message. */
|
|
10
|
+
export interface PasteBlock {
|
|
11
|
+
/** A stable host-provided ASCII identifier; never included in visible token text. */
|
|
12
|
+
id: string
|
|
13
|
+
/** Positive safe integer, unique among blocks in the same text value. */
|
|
14
|
+
seq: number
|
|
15
|
+
/** Positive safe integer displayed in the token. */
|
|
16
|
+
lines: number
|
|
17
|
+
/** Original pasted text, retained verbatim. */
|
|
18
|
+
content: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const PASTE_THRESHOLD_LINES = 3
|
|
22
|
+
export const PASTE_THRESHOLD_CHARS = 200
|
|
23
|
+
|
|
24
|
+
const PASTE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
|
|
25
|
+
const MAX_SAFE_SEQUENCE_TEXT_LENGTH = String(Number.MAX_SAFE_INTEGER).length
|
|
26
|
+
const PASTE_TOKEN_SOURCE = String.raw`\[ Paste #([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) · ([1-9]\d{0,${MAX_SAFE_SEQUENCE_TEXT_LENGTH - 1}}) lines \]`
|
|
27
|
+
|
|
28
|
+
/** Canonical token pattern. Internal scans always use a fresh RegExp instance. */
|
|
29
|
+
export const PASTE_TOKEN_REGEX = new RegExp(PASTE_TOKEN_SOURCE, 'g')
|
|
30
|
+
|
|
31
|
+
function isPositiveSafeInteger(value: unknown): value is number {
|
|
32
|
+
return Number.isSafeInteger(value) && (value as number) > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Check data received from a host before using it as a paste-token block. */
|
|
36
|
+
export function isPasteBlock(value: unknown): value is PasteBlock {
|
|
37
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
38
|
+
const candidate = value as Record<string, unknown>
|
|
39
|
+
return (
|
|
40
|
+
typeof candidate.id === 'string' &&
|
|
41
|
+
PASTE_ID_PATTERN.test(candidate.id) &&
|
|
42
|
+
isPositiveSafeInteger(candidate.seq) &&
|
|
43
|
+
isPositiveSafeInteger(candidate.lines) &&
|
|
44
|
+
typeof candidate.content === 'string'
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function unambiguousBlocks(blocks: readonly PasteBlock[]): PasteBlock[] {
|
|
49
|
+
const valid = blocks.filter(isPasteBlock)
|
|
50
|
+
const seqCounts = new Map<number, number>()
|
|
51
|
+
const idCounts = new Map<string, number>()
|
|
52
|
+
for (const block of valid) {
|
|
53
|
+
seqCounts.set(block.seq, (seqCounts.get(block.seq) ?? 0) + 1)
|
|
54
|
+
idCounts.set(block.id, (idCounts.get(block.id) ?? 0) + 1)
|
|
55
|
+
}
|
|
56
|
+
return valid.filter((block) => seqCounts.get(block.seq) === 1 && idCounts.get(block.id) === 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatToken(block: PasteBlock): string {
|
|
60
|
+
if (!isPasteBlock(block)) throw new TypeError('Cannot format an invalid paste block')
|
|
61
|
+
return `[ Paste #${block.seq} · ${block.lines} lines ]`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function shouldCollapse(text: string): boolean {
|
|
65
|
+
if (!text) return false
|
|
66
|
+
return countLines(text) >= PASTE_THRESHOLD_LINES || text.length >= PASTE_THRESHOLD_CHARS
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function countLines(text: string): number {
|
|
70
|
+
if (!text) return 0
|
|
71
|
+
return text.split('\n').length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Next sequence for a new paste = max existing + 1, starting at 1. */
|
|
75
|
+
export function nextSeq(blocks: readonly PasteBlock[]): number {
|
|
76
|
+
let max = 0
|
|
77
|
+
for (const block of blocks) {
|
|
78
|
+
if (isPasteBlock(block) && block.seq > max) max = block.seq
|
|
79
|
+
}
|
|
80
|
+
if (max === Number.MAX_SAFE_INTEGER) throw new RangeError('Paste sequence space is exhausted')
|
|
81
|
+
return max + 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Re-sequence `carried` blocks whose sequence is already taken by `used`, and
|
|
86
|
+
* rewrite their markers in one right-to-left pass. Carried block IDs and
|
|
87
|
+
* sequences must be unique so a visible marker always has one backing block.
|
|
88
|
+
* `used` is mutated to include the assigned sequences.
|
|
89
|
+
*/
|
|
90
|
+
export function remapCarriedBlocks(
|
|
91
|
+
text: string,
|
|
92
|
+
carried: readonly PasteBlock[],
|
|
93
|
+
used: Set<number>
|
|
94
|
+
): { text: string; blocks: PasteBlock[] } {
|
|
95
|
+
const seenSeqs = new Set<number>()
|
|
96
|
+
const seenIds = new Set<string>()
|
|
97
|
+
for (const block of carried) {
|
|
98
|
+
if (!isPasteBlock(block)) throw new TypeError('Cannot remap an invalid paste block')
|
|
99
|
+
if (seenSeqs.has(block.seq) || seenIds.has(block.id)) {
|
|
100
|
+
throw new TypeError('Carried paste block IDs and sequences must be unique')
|
|
101
|
+
}
|
|
102
|
+
seenSeqs.add(block.seq)
|
|
103
|
+
seenIds.add(block.id)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let maxUsed = 0
|
|
107
|
+
for (const value of used) {
|
|
108
|
+
if (isPositiveSafeInteger(value) && value > maxUsed) maxUsed = value
|
|
109
|
+
}
|
|
110
|
+
let free = maxUsed < Number.MAX_SAFE_INTEGER ? maxUsed + 1 : 1
|
|
111
|
+
const allocateFree = (): number => {
|
|
112
|
+
while (used.has(free)) {
|
|
113
|
+
free = free < Number.MAX_SAFE_INTEGER ? free + 1 : 1
|
|
114
|
+
}
|
|
115
|
+
const assigned = free
|
|
116
|
+
used.add(assigned)
|
|
117
|
+
free = assigned < Number.MAX_SAFE_INTEGER ? assigned + 1 : 1
|
|
118
|
+
return assigned
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const remap = new Map<number, number>()
|
|
122
|
+
const blocks = carried.map((block) => {
|
|
123
|
+
if (!used.has(block.seq)) {
|
|
124
|
+
used.add(block.seq)
|
|
125
|
+
return block
|
|
126
|
+
}
|
|
127
|
+
const seq = allocateFree()
|
|
128
|
+
remap.set(block.seq, seq)
|
|
129
|
+
return { ...block, seq }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (!remap.size) return { text, blocks }
|
|
133
|
+
|
|
134
|
+
let out = text
|
|
135
|
+
const ranges = findTokenRanges(text, carried)
|
|
136
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
137
|
+
const { start, end, block } = ranges[index]!
|
|
138
|
+
const mapped = remap.get(block.seq)
|
|
139
|
+
if (mapped === undefined) continue
|
|
140
|
+
out = out.slice(0, start) + formatToken({ ...block, seq: mapped }) + out.slice(end)
|
|
141
|
+
}
|
|
142
|
+
return { text: out, blocks }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Ranges for canonical tokens whose sequence and line count match one block. */
|
|
146
|
+
export function findTokenRanges(
|
|
147
|
+
text: string,
|
|
148
|
+
blocks: readonly PasteBlock[]
|
|
149
|
+
): Array<{ start: number; end: number; block: PasteBlock }> {
|
|
150
|
+
if (!text || !blocks.length) return []
|
|
151
|
+
const bySeq = new Map(unambiguousBlocks(blocks).map((block) => [block.seq, block]))
|
|
152
|
+
if (!bySeq.size) return []
|
|
153
|
+
|
|
154
|
+
const tokenRegex = new RegExp(PASTE_TOKEN_SOURCE, 'g')
|
|
155
|
+
const ranges: Array<{ start: number; end: number; block: PasteBlock }> = []
|
|
156
|
+
let match: RegExpExecArray | null
|
|
157
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
158
|
+
const seq = Number(match[1])
|
|
159
|
+
const lines = Number(match[2])
|
|
160
|
+
if (!isPositiveSafeInteger(seq) || !isPositiveSafeInteger(lines)) continue
|
|
161
|
+
const block = bySeq.get(seq)
|
|
162
|
+
if (block && block.lines === lines) {
|
|
163
|
+
ranges.push({ start: match.index, end: match.index + match[0].length, block })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return ranges
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function tokenRangeAt(
|
|
170
|
+
text: string,
|
|
171
|
+
blocks: readonly PasteBlock[],
|
|
172
|
+
caret: number
|
|
173
|
+
): { start: number; end: number; block: PasteBlock } | null {
|
|
174
|
+
if (!Number.isSafeInteger(caret) || caret < 0 || caret > text.length) return null
|
|
175
|
+
for (const range of findTokenRanges(text, blocks)) {
|
|
176
|
+
if (caret >= range.start && caret <= range.end) return range
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function pruneBlocks(text: string, blocks: PasteBlock[]): PasteBlock[] {
|
|
182
|
+
if (!blocks.length) return blocks
|
|
183
|
+
const usable = unambiguousBlocks(blocks)
|
|
184
|
+
const survivors = new Set(findTokenRanges(text, usable).map((range) => range.block.id))
|
|
185
|
+
const next = usable.filter((block) => survivors.has(block.id))
|
|
186
|
+
return next.length === blocks.length && next.every((block, index) => block === blocks[index])
|
|
187
|
+
? blocks
|
|
188
|
+
: next
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function expandAll(text: string, blocks: readonly PasteBlock[]): string {
|
|
192
|
+
if (!text || !blocks.length) return text
|
|
193
|
+
const ranges = findTokenRanges(text, blocks)
|
|
194
|
+
if (!ranges.length) return text
|
|
195
|
+
let out = text
|
|
196
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
197
|
+
const range = ranges[index]!
|
|
198
|
+
out = out.slice(0, range.start) + range.block.content + out.slice(range.end)
|
|
199
|
+
}
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Inverse of `expandAll`: replace each first non-overlapping verbatim block. */
|
|
204
|
+
export function recollapsePastes(content: string, blocks: readonly PasteBlock[]): string {
|
|
205
|
+
if (!content || !blocks.length) return content
|
|
206
|
+
|
|
207
|
+
interface Hit {
|
|
208
|
+
start: number
|
|
209
|
+
end: number
|
|
210
|
+
block: PasteBlock
|
|
211
|
+
}
|
|
212
|
+
const hits: Hit[] = []
|
|
213
|
+
const claimed: Array<[number, number]> = []
|
|
214
|
+
const firstUnclaimed = (needle: string): number => {
|
|
215
|
+
if (!needle) return -1
|
|
216
|
+
let from = 0
|
|
217
|
+
while (from <= content.length) {
|
|
218
|
+
const index = content.indexOf(needle, from)
|
|
219
|
+
if (index < 0) return -1
|
|
220
|
+
if (!claimed.some(([start, end]) => index < end && index + needle.length > start))
|
|
221
|
+
return index
|
|
222
|
+
from = index + 1
|
|
223
|
+
}
|
|
224
|
+
return -1
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const block of unambiguousBlocks(blocks)) {
|
|
228
|
+
if (!block.content) continue
|
|
229
|
+
const trimmed = block.content.trimEnd()
|
|
230
|
+
let needle = block.content
|
|
231
|
+
let index = firstUnclaimed(needle)
|
|
232
|
+
if (index < 0 && trimmed && trimmed !== block.content) {
|
|
233
|
+
needle = trimmed
|
|
234
|
+
index = firstUnclaimed(needle)
|
|
235
|
+
}
|
|
236
|
+
if (index < 0) continue
|
|
237
|
+
const end = index + needle.length
|
|
238
|
+
hits.push({ start: index, end, block })
|
|
239
|
+
claimed.push([index, end])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!hits.length) return content
|
|
243
|
+
hits.sort((left, right) => left.start - right.start)
|
|
244
|
+
let out = ''
|
|
245
|
+
let position = 0
|
|
246
|
+
for (const hit of hits) {
|
|
247
|
+
if (hit.start < position) continue
|
|
248
|
+
out += content.slice(position, hit.start) + formatToken(hit.block)
|
|
249
|
+
position = hit.end
|
|
250
|
+
}
|
|
251
|
+
return out + content.slice(position)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Strip a trailing whitespace run only when that run contains a newline. */
|
|
255
|
+
export function stripTrailingBlankLines(value: string): string {
|
|
256
|
+
let index = value.length - 1
|
|
257
|
+
let sawNewline = false
|
|
258
|
+
while (index >= 0) {
|
|
259
|
+
const code = value.charCodeAt(index)
|
|
260
|
+
if (code === 10 || code === 13) {
|
|
261
|
+
sawNewline = true
|
|
262
|
+
index -= 1
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (code === 32 || code === 9) {
|
|
266
|
+
index -= 1
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
return sawNewline ? value.slice(0, index + 1) : value
|
|
272
|
+
}
|