@marimo-team/frontend 0.23.2-dev65 → 0.23.2-dev68
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/dist/assets/JsonOutput-sWUD3O5F.js +49 -0
- package/dist/assets/{add-connection-dialog-eBEz1Rlx.js → add-connection-dialog-D6XFTnTb.js} +1 -1
- package/dist/assets/{agent-panel-DdQIPV4s.js → agent-panel-D7n2noTX.js} +1 -1
- package/dist/assets/{cell-editor-DTCP63YJ.js → cell-editor-BUrhiKcX.js} +1 -1
- package/dist/assets/{column-preview-M5tW9DeW.js → column-preview-BbSZl4gC.js} +1 -1
- package/dist/assets/{command-palette-ExiY3B81.js → command-palette-Cc7XXZHG.js} +1 -1
- package/dist/assets/{edit-page-BTwAqki-.js → edit-page-CK943x81.js} +3 -3
- package/dist/assets/{file-explorer-panel-CuPnqR_c.js → file-explorer-panel-B5sFxYu1.js} +1 -1
- package/dist/assets/{form-D76nZZF7.js → form-BUVFJb5I.js} +1 -1
- package/dist/assets/{hooks-BSyCiOiV.js → hooks-CK1ac3iV.js} +1 -1
- package/dist/assets/index-BuOIqA8d.css +2 -0
- package/dist/assets/{index-BRf5dlJi.js → index-DCq7udug.js} +3 -3
- package/dist/assets/{layout-BULJlgp-.js → layout-0Xp7ABJw.js} +3 -3
- package/dist/assets/{panels-D7Ix_ZZ-.js → panels-s8gQr6G_.js} +1 -1
- package/dist/assets/{reveal-component-BFqPeIsQ.js → reveal-component-CT2scTNG.js} +1 -1
- package/dist/assets/{run-page-LlaD7bx4.js → run-page-BebSJHhp.js} +1 -1
- package/dist/assets/{scratchpad-panel-DEAn87uP.js → scratchpad-panel-CPn_yM49.js} +1 -1
- package/dist/assets/{session-panel-DrVK3__u.js → session-panel-CfLYfSX2.js} +1 -1
- package/dist/assets/{slide-szdKdcoM.js → slide-CdqatvaH.js} +1 -1
- package/dist/assets/{state-Do4CKYxK.js → state-BFFSWzNE.js} +1 -1
- package/dist/assets/{useNotebookActions-maZjRwJd.js → useNotebookActions-BPt4w3KQ.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/src/components/data-table/__tests__/columns.test.tsx +104 -0
- package/src/components/data-table/__tests__/sentinel-cell.test.tsx +89 -1
- package/src/components/data-table/__tests__/utils.test.ts +99 -0
- package/src/components/data-table/columns.tsx +34 -5
- package/src/components/data-table/sentinel-cell.tsx +34 -6
- package/src/components/data-table/utils.ts +45 -0
- package/dist/assets/JsonOutput-B7SdSwth.js +0 -49
- package/dist/assets/index-CD0jEnvj.css +0 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { render } from "@testing-library/react";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { SentinelCell } from "../sentinel-cell";
|
|
5
|
+
import { SentinelCell, WhitespaceMarkers } from "../sentinel-cell";
|
|
6
6
|
import type { CellValueSentinel } from "../types";
|
|
7
7
|
|
|
8
8
|
function renderSentinel(sentinel: CellValueSentinel) {
|
|
@@ -10,6 +10,10 @@ function renderSentinel(sentinel: CellValueSentinel) {
|
|
|
10
10
|
return container.querySelector("span")!;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function renderMarkers(value: string) {
|
|
14
|
+
return render(<WhitespaceMarkers value={value} />);
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
describe("SentinelCell", () => {
|
|
14
18
|
it("renders null as None", () => {
|
|
15
19
|
const span = renderSentinel({ type: "null", value: null });
|
|
@@ -81,3 +85,87 @@ describe("SentinelCell", () => {
|
|
|
81
85
|
expect(span.getAttribute("title")).toBe("NaT (Not a Time)");
|
|
82
86
|
});
|
|
83
87
|
});
|
|
88
|
+
|
|
89
|
+
describe("WhitespaceMarkers", () => {
|
|
90
|
+
it("renders nothing for empty string", () => {
|
|
91
|
+
const { container } = renderMarkers("");
|
|
92
|
+
expect(container.firstChild).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("renders a single space as open box", () => {
|
|
96
|
+
const { container } = renderMarkers(" ");
|
|
97
|
+
const outer = container.querySelector("span")!;
|
|
98
|
+
expect(outer.textContent).toBe("\u2423");
|
|
99
|
+
expect(outer.getAttribute("aria-label")).toBe("1 space");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders multiple spaces as multiple open boxes", () => {
|
|
103
|
+
const { container } = renderMarkers(" ");
|
|
104
|
+
const outer = container.querySelector("span")!;
|
|
105
|
+
expect(outer.textContent).toBe("\u2423\u2423\u2423");
|
|
106
|
+
expect(outer.getAttribute("aria-label")).toBe("3 spaces");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders tab, newline, CR with escape labels", () => {
|
|
110
|
+
const { container } = renderMarkers("\t\n\r");
|
|
111
|
+
const outer = container.querySelector("span")!;
|
|
112
|
+
expect(outer.textContent).toBe("\\t\\n\\r");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders each char in its own span for CSS spacing", () => {
|
|
116
|
+
const { container } = renderMarkers(" ");
|
|
117
|
+
const outer = container.querySelector("span")!;
|
|
118
|
+
// Outer wrapper + three inner spans (one per char)
|
|
119
|
+
expect(outer.querySelectorAll("span")).toHaveLength(3);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("renders unknown whitespace (NBSP) as \\uXXXX escape", () => {
|
|
123
|
+
const { container } = renderMarkers("\u00a0");
|
|
124
|
+
const outer = container.querySelector("span")!;
|
|
125
|
+
expect(outer.textContent).toBe("\\u00a0");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("renders BOM as \\ufeff", () => {
|
|
129
|
+
const { container } = renderMarkers("\ufeff");
|
|
130
|
+
const outer = container.querySelector("span")!;
|
|
131
|
+
expect(outer.textContent).toBe("\\ufeff");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders en space and em space as escapes", () => {
|
|
135
|
+
const { container } = renderMarkers("\u2002\u2003");
|
|
136
|
+
const outer = container.querySelector("span")!;
|
|
137
|
+
expect(outer.textContent).toBe("\\u2002\\u2003");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("mixes known glyphs and unknown escapes correctly", () => {
|
|
141
|
+
const { container } = renderMarkers(" \t\u00a0");
|
|
142
|
+
const outer = container.querySelector("span")!;
|
|
143
|
+
expect(outer.textContent).toBe("\u2423\\t\\u00a0");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("describes mixed whitespace in aria-label", () => {
|
|
147
|
+
const { container } = renderMarkers(" \t\n");
|
|
148
|
+
const outer = container.querySelector("span")!;
|
|
149
|
+
expect(outer.getAttribute("aria-label")).toBe("1 space, 1 tab, 1 newline");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("describes unknown whitespace as 'unicode whitespace'", () => {
|
|
153
|
+
const { container } = renderMarkers("\u00a0");
|
|
154
|
+
const outer = container.querySelector("span")!;
|
|
155
|
+
expect(outer.getAttribute("aria-label")).toBe("1 unicode whitespace");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("pluralizes unknown whitespace in aria-label", () => {
|
|
159
|
+
const { container } = renderMarkers("\u00a0\u00a0\u2002");
|
|
160
|
+
const outer = container.querySelector("span")!;
|
|
161
|
+
expect(outer.getAttribute("aria-label")).toBe("3 unicode whitespaces");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("mixes known and unknown whitespace labels", () => {
|
|
165
|
+
const { container } = renderMarkers(" \u00a0\t");
|
|
166
|
+
const outer = container.querySelector("span")!;
|
|
167
|
+
expect(outer.getAttribute("aria-label")).toBe(
|
|
168
|
+
"1 space, 1 unicode whitespace, 1 tab",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getClipboardContent,
|
|
8
8
|
getPageIndexForRow,
|
|
9
9
|
getRawValue,
|
|
10
|
+
splitLeadingTrailingWhitespace,
|
|
10
11
|
stringifyUnknownValue,
|
|
11
12
|
} from "../utils";
|
|
12
13
|
|
|
@@ -342,3 +343,101 @@ describe("getRawValue", () => {
|
|
|
342
343
|
expect(getRawValue(table, 5, "a")).toBeUndefined();
|
|
343
344
|
});
|
|
344
345
|
});
|
|
346
|
+
|
|
347
|
+
describe("splitLeadingTrailingWhitespace", () => {
|
|
348
|
+
it("returns all empty for empty string", () => {
|
|
349
|
+
expect(splitLeadingTrailingWhitespace("")).toEqual({
|
|
350
|
+
leading: "",
|
|
351
|
+
middle: "",
|
|
352
|
+
trailing: "",
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("returns value as middle when no edge whitespace", () => {
|
|
357
|
+
expect(splitLeadingTrailingWhitespace("abc")).toEqual({
|
|
358
|
+
leading: "",
|
|
359
|
+
middle: "abc",
|
|
360
|
+
trailing: "",
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("preserves inner whitespace in middle", () => {
|
|
365
|
+
expect(splitLeadingTrailingWhitespace("abc d ef")).toEqual({
|
|
366
|
+
leading: "",
|
|
367
|
+
middle: "abc d ef",
|
|
368
|
+
trailing: "",
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("splits leading whitespace only", () => {
|
|
373
|
+
expect(splitLeadingTrailingWhitespace(" abc")).toEqual({
|
|
374
|
+
leading: " ",
|
|
375
|
+
middle: "abc",
|
|
376
|
+
trailing: "",
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("splits trailing whitespace only", () => {
|
|
381
|
+
expect(splitLeadingTrailingWhitespace("abc ")).toEqual({
|
|
382
|
+
leading: "",
|
|
383
|
+
middle: "abc",
|
|
384
|
+
trailing: " ",
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("splits both leading and trailing whitespace", () => {
|
|
389
|
+
expect(splitLeadingTrailingWhitespace(" abc ")).toEqual({
|
|
390
|
+
leading: " ",
|
|
391
|
+
middle: "abc",
|
|
392
|
+
trailing: " ",
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("handles mixed whitespace types at edges", () => {
|
|
397
|
+
expect(splitLeadingTrailingWhitespace("\t\n abc \r\t")).toEqual({
|
|
398
|
+
leading: "\t\n ",
|
|
399
|
+
middle: "abc",
|
|
400
|
+
trailing: " \r\t",
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("preserves inner whitespace when edges have whitespace", () => {
|
|
405
|
+
expect(splitLeadingTrailingWhitespace(" a b c ")).toEqual({
|
|
406
|
+
leading: " ",
|
|
407
|
+
middle: "a b c",
|
|
408
|
+
trailing: " ",
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("handles Unicode whitespace (NBSP) at edges", () => {
|
|
413
|
+
expect(splitLeadingTrailingWhitespace("\u00a0abc\u00a0")).toEqual({
|
|
414
|
+
leading: "\u00a0",
|
|
415
|
+
middle: "abc",
|
|
416
|
+
trailing: "\u00a0",
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("puts whitespace-only string in leading (caller should handle sentinel first)", () => {
|
|
421
|
+
expect(splitLeadingTrailingWhitespace(" ")).toEqual({
|
|
422
|
+
leading: " ",
|
|
423
|
+
middle: "",
|
|
424
|
+
trailing: "",
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("handles single whitespace char", () => {
|
|
429
|
+
expect(splitLeadingTrailingWhitespace(" ")).toEqual({
|
|
430
|
+
leading: " ",
|
|
431
|
+
middle: "",
|
|
432
|
+
trailing: "",
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("handles single non-whitespace char", () => {
|
|
437
|
+
expect(splitLeadingTrailingWhitespace("a")).toEqual({
|
|
438
|
+
leading: "",
|
|
439
|
+
middle: "a",
|
|
440
|
+
trailing: "",
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
INDEX_COLUMN_NAME,
|
|
40
40
|
isNumericType,
|
|
41
41
|
} from "./types";
|
|
42
|
-
import { SentinelCell } from "./sentinel-cell";
|
|
43
|
-
import { detectSentinel } from "./utils";
|
|
42
|
+
import { SentinelCell, WhitespaceMarkers } from "./sentinel-cell";
|
|
43
|
+
import { detectSentinel, splitLeadingTrailingWhitespace } from "./utils";
|
|
44
44
|
import { uniformSample } from "./uniformSample";
|
|
45
45
|
import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
|
|
46
46
|
|
|
@@ -346,6 +346,7 @@ const PopoutColumn = ({
|
|
|
346
346
|
cellStyles,
|
|
347
347
|
selectCell,
|
|
348
348
|
rawStringValue,
|
|
349
|
+
edges,
|
|
349
350
|
contentClassName,
|
|
350
351
|
buttonText,
|
|
351
352
|
wrapped,
|
|
@@ -354,11 +355,25 @@ const PopoutColumn = ({
|
|
|
354
355
|
cellStyles?: string;
|
|
355
356
|
selectCell?: () => void;
|
|
356
357
|
rawStringValue: string;
|
|
358
|
+
// Edge whitespace shown as visible markers in the trigger; copy/title
|
|
359
|
+
// still use `rawStringValue`. Middle is sliced from `rawStringValue`.
|
|
360
|
+
edges?: { leading: string; trailing: string };
|
|
357
361
|
contentClassName?: string;
|
|
358
362
|
buttonText?: string;
|
|
359
363
|
wrapped?: boolean;
|
|
360
364
|
children: React.ReactNode;
|
|
361
365
|
}) => {
|
|
366
|
+
const hasEdgeWhitespace =
|
|
367
|
+
edges !== undefined &&
|
|
368
|
+
(edges.leading.length > 0 || edges.trailing.length > 0);
|
|
369
|
+
|
|
370
|
+
const displayText = hasEdgeWhitespace
|
|
371
|
+
? rawStringValue.slice(
|
|
372
|
+
edges.leading.length,
|
|
373
|
+
rawStringValue.length - edges.trailing.length,
|
|
374
|
+
)
|
|
375
|
+
: rawStringValue;
|
|
376
|
+
|
|
362
377
|
return (
|
|
363
378
|
<EmotionCacheProvider container={null}>
|
|
364
379
|
<Popover>
|
|
@@ -377,7 +392,9 @@ const PopoutColumn = ({
|
|
|
377
392
|
)}
|
|
378
393
|
title={rawStringValue}
|
|
379
394
|
>
|
|
380
|
-
{
|
|
395
|
+
{edges ? <WhitespaceMarkers value={edges.leading} /> : null}
|
|
396
|
+
{displayText}
|
|
397
|
+
{edges ? <WhitespaceMarkers value={edges.trailing} /> : null}
|
|
381
398
|
</span>
|
|
382
399
|
</PopoverTrigger>
|
|
383
400
|
<PopoverContent
|
|
@@ -582,7 +599,13 @@ export function renderCellValue<TData, TValue>({
|
|
|
582
599
|
? String(column.applyColumnFormatting(value))
|
|
583
600
|
: String(renderValue());
|
|
584
601
|
|
|
585
|
-
const
|
|
602
|
+
const { leading, middle, trailing } =
|
|
603
|
+
splitLeadingTrailingWhitespace(stringValue);
|
|
604
|
+
const hasEdgeWhitespace = leading.length > 0 || trailing.length > 0;
|
|
605
|
+
|
|
606
|
+
// Parse only the inner content for URL detection so URLDetector doesn't
|
|
607
|
+
// split on the whitespace padding.
|
|
608
|
+
const parts = parseContent(hasEdgeWhitespace ? middle : stringValue);
|
|
586
609
|
const allMarkup = parts.every((part) => part.type !== "text");
|
|
587
610
|
if (allMarkup || stringValue.length < MAX_STRING_LENGTH || isWrapped) {
|
|
588
611
|
return (
|
|
@@ -590,7 +613,9 @@ export function renderCellValue<TData, TValue>({
|
|
|
590
613
|
onClick={selectCell}
|
|
591
614
|
className={cn(cellStyles, isWrapped && COLUMN_WRAPPING_STYLES)}
|
|
592
615
|
>
|
|
616
|
+
<WhitespaceMarkers value={leading} />
|
|
593
617
|
<UrlDetector parts={parts} />
|
|
618
|
+
<WhitespaceMarkers value={trailing} />
|
|
594
619
|
</div>
|
|
595
620
|
);
|
|
596
621
|
}
|
|
@@ -600,11 +625,15 @@ export function renderCellValue<TData, TValue>({
|
|
|
600
625
|
cellStyles={cellStyles}
|
|
601
626
|
selectCell={selectCell}
|
|
602
627
|
rawStringValue={stringValue}
|
|
628
|
+
edges={{ leading, trailing }}
|
|
603
629
|
contentClassName="max-h-64 overflow-auto whitespace-pre-wrap break-words text-sm w-96"
|
|
604
630
|
buttonText="X"
|
|
605
631
|
wrapped={isWrapped}
|
|
606
632
|
>
|
|
607
|
-
<MarkdownUrlDetector
|
|
633
|
+
<MarkdownUrlDetector
|
|
634
|
+
content={stringValue}
|
|
635
|
+
parts={parseContent(stringValue)}
|
|
636
|
+
/>
|
|
608
637
|
</PopoutColumn>
|
|
609
638
|
);
|
|
610
639
|
}
|
|
@@ -3,20 +3,30 @@
|
|
|
3
3
|
import type { CellValueSentinel, CellValueSentinelType } from "./types";
|
|
4
4
|
|
|
5
5
|
const WHITESPACE_CHARS: Record<string, { marker: string; name: string }> = {
|
|
6
|
-
" ": { marker: "\u2423", name: "space" },
|
|
6
|
+
" ": { marker: "\u2423", name: "space" },
|
|
7
7
|
"\t": { marker: "\\t", name: "tab" },
|
|
8
8
|
"\n": { marker: "\\n", name: "newline" },
|
|
9
|
-
"\r": { marker: "\\r", name: "
|
|
9
|
+
"\r": { marker: "\\r", name: "carriage return" },
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
function renderWhitespaceMarkers(str: string):
|
|
13
|
-
return [...str].map((ch) =>
|
|
12
|
+
function renderWhitespaceMarkers(str: string): React.ReactNode[] {
|
|
13
|
+
return [...str].map((ch, i) => {
|
|
14
|
+
const entry = WHITESPACE_CHARS[ch];
|
|
15
|
+
const marker = entry
|
|
16
|
+
? entry.marker
|
|
17
|
+
: `\\u${(ch.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`;
|
|
18
|
+
return (
|
|
19
|
+
<span key={i} className="mr-0.5 last:mr-0">
|
|
20
|
+
{marker}
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
function describeWhitespace(str: string): string {
|
|
17
27
|
const counts: Record<string, number> = {};
|
|
18
28
|
for (const ch of str) {
|
|
19
|
-
const name = WHITESPACE_CHARS[ch]?.name ?? "
|
|
29
|
+
const name = WHITESPACE_CHARS[ch]?.name ?? "unicode whitespace";
|
|
20
30
|
counts[name] = (counts[name] ?? 0) + 1;
|
|
21
31
|
}
|
|
22
32
|
return Object.entries(counts)
|
|
@@ -25,7 +35,7 @@ function describeWhitespace(str: string): string {
|
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
interface SentinelConfig {
|
|
28
|
-
label: (value: CellValueSentinel["value"]) => string;
|
|
38
|
+
label: (value: CellValueSentinel["value"]) => string | React.ReactNode[];
|
|
29
39
|
tooltip: (value: CellValueSentinel["value"]) => string;
|
|
30
40
|
ariaLabel: (value: CellValueSentinel["value"]) => string;
|
|
31
41
|
}
|
|
@@ -68,6 +78,24 @@ const SENTINEL_CONFIG: Record<CellValueSentinelType, SentinelConfig> = {
|
|
|
68
78
|
},
|
|
69
79
|
};
|
|
70
80
|
|
|
81
|
+
export function WhitespaceMarkers({ value }: { value: string }) {
|
|
82
|
+
if (!value) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const description = describeWhitespace(value);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<span
|
|
90
|
+
className="text-muted-foreground opacity-60"
|
|
91
|
+
aria-label={description}
|
|
92
|
+
title={description}
|
|
93
|
+
>
|
|
94
|
+
{renderWhitespaceMarkers(value)}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
export function SentinelCell({
|
|
72
100
|
sentinel,
|
|
73
101
|
}: {
|
|
@@ -14,6 +14,51 @@ import {
|
|
|
14
14
|
} from "./types";
|
|
15
15
|
|
|
16
16
|
const WHITESPACE_ONLY_RE = /^[\s]+$/;
|
|
17
|
+
const WHITESPACE_CHAR_RE = /\s/;
|
|
18
|
+
const EDGE_WHITESPACE_RE = /^(\s*)([\s\S]*?)(\s*)$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* checks for leading and trailing whitespaces.
|
|
22
|
+
* Will run for every cell, so fast exits for common cases
|
|
23
|
+
*
|
|
24
|
+
* @param value - to split
|
|
25
|
+
* @returns - leading, middle, and trailing string where leading and trailing are whitespace string
|
|
26
|
+
*/
|
|
27
|
+
export function splitLeadingTrailingWhitespace(value: string): {
|
|
28
|
+
leading: string;
|
|
29
|
+
middle: string;
|
|
30
|
+
trailing: string;
|
|
31
|
+
} {
|
|
32
|
+
const parts = {
|
|
33
|
+
leading: "",
|
|
34
|
+
middle: "",
|
|
35
|
+
trailing: "",
|
|
36
|
+
};
|
|
37
|
+
if (value.length === 0) {
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const firstWhitespaceCh = WHITESPACE_CHAR_RE.test(value[0]);
|
|
42
|
+
const lastWhitespaceCh = WHITESPACE_CHAR_RE.test(value[value.length - 1]);
|
|
43
|
+
|
|
44
|
+
// if does not start or end with ws
|
|
45
|
+
if (!firstWhitespaceCh && !lastWhitespaceCh) {
|
|
46
|
+
parts.middle = value;
|
|
47
|
+
return parts;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const match = EDGE_WHITESPACE_RE.exec(value);
|
|
51
|
+
if (!match) {
|
|
52
|
+
parts.middle = value;
|
|
53
|
+
return parts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parts.leading = match[1] ?? "";
|
|
57
|
+
parts.middle = match[2] ?? "";
|
|
58
|
+
parts.trailing = match[3] ?? "";
|
|
59
|
+
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
17
62
|
|
|
18
63
|
/**
|
|
19
64
|
* Convenience function to load table data.
|