@ultimat3/ui 29.3.2 → 30.0.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/CATALOG.md +491 -24
- package/CLAUDE.md +23 -16
- package/README.md +338 -33
- package/package.json +16 -5
- package/src/components/AppShell.module.scss +23 -7
- package/src/components/AppShell.tsx +9 -0
- package/src/components/BarChart.module.scss +26 -0
- package/src/components/BarChart.tsx +242 -93
- package/src/components/Board.module.scss +30 -0
- package/src/components/Board.tsx +47 -0
- package/src/components/BottomNav.module.scss +49 -0
- package/src/components/BottomNav.tsx +57 -0
- package/src/components/Calendar.module.scss +118 -0
- package/src/components/Calendar.tsx +246 -0
- package/src/components/ChartFrame.module.scss +40 -3
- package/src/components/ChartFrame.tsx +84 -28
- package/src/components/CodeBlock.module.scss +101 -0
- package/src/components/CodeBlock.tsx +76 -0
- package/src/components/CommandPalette.module.scss +25 -0
- package/src/components/CommandPalette.tsx +50 -22
- package/src/components/ConfirmDialog.module.scss +36 -0
- package/src/components/ConfirmDialog.tsx +188 -0
- package/src/components/DataTable.tsx +92 -33
- package/src/components/DatePicker.module.scss +37 -0
- package/src/components/DatePicker.tsx +134 -0
- package/src/components/DateRangePicker.module.scss +54 -0
- package/src/components/DateRangePicker.tsx +156 -0
- package/src/components/DescriptionList.module.scss +46 -0
- package/src/components/DescriptionList.tsx +38 -0
- package/src/components/DiffView.module.scss +124 -0
- package/src/components/DiffView.tsx +169 -0
- package/src/components/DonutChart.tsx +13 -4
- package/src/components/Drawer.module.scss +40 -0
- package/src/components/Drawer.tsx +56 -0
- package/src/components/Gauge.module.scss +8 -0
- package/src/components/Gauge.tsx +40 -13
- package/src/components/HeatStrip.module.scss +133 -0
- package/src/components/HeatStrip.tsx +64 -0
- package/src/components/JsonView.module.scss +64 -0
- package/src/components/JsonView.tsx +68 -0
- package/src/components/LineChart.tsx +45 -18
- package/src/components/LiveIndicator.module.scss +44 -0
- package/src/components/LiveIndicator.tsx +45 -0
- package/src/components/LogViewer.module.scss +102 -0
- package/src/components/LogViewer.tsx +123 -0
- package/src/components/Markdown.module.scss +87 -0
- package/src/components/Markdown.tsx +84 -0
- package/src/components/Meter.module.scss +7 -0
- package/src/components/Meter.tsx +16 -0
- package/src/components/NavList.module.scss +66 -0
- package/src/components/NavList.tsx +95 -0
- package/src/components/Pagination.module.scss +6 -2
- package/src/components/Pagination.tsx +57 -13
- package/src/components/Popover.tsx +12 -2
- package/src/components/PullToRefresh.module.scss +41 -0
- package/src/components/PullToRefresh.tsx +108 -0
- package/src/components/Radio.module.scss +51 -0
- package/src/components/Radio.tsx +12 -1
- package/src/components/SearchableSelect.module.scss +216 -0
- package/src/components/SearchableSelect.tsx +331 -0
- package/src/components/SearchableSelectChips.tsx +41 -0
- package/src/components/SearchableSelectNative.tsx +89 -0
- package/src/components/ShortcutSheet.module.scss +33 -0
- package/src/components/ShortcutSheet.tsx +67 -0
- package/src/components/Slider.module.scss +50 -0
- package/src/components/Slider.tsx +68 -0
- package/src/components/SortableList.module.scss +72 -0
- package/src/components/SortableList.tsx +42 -0
- package/src/components/Sparkline.module.scss +10 -2
- package/src/components/Sparkline.tsx +27 -10
- package/src/components/SplitPane.module.scss +39 -0
- package/src/components/SplitPane.tsx +96 -0
- package/src/components/StatTile.module.scss +29 -3
- package/src/components/StatTile.tsx +75 -16
- package/src/components/Stepper.module.scss +78 -0
- package/src/components/Stepper.tsx +73 -0
- package/src/components/Tabs.module.scss +13 -1
- package/src/components/Tabs.tsx +77 -1
- package/src/components/TerminalView.module.scss +68 -0
- package/src/components/TerminalView.tsx +97 -0
- package/src/components/TimeRangePicker.module.scss +85 -0
- package/src/components/TimeRangePicker.tsx +91 -0
- package/src/components/Timeline.module.scss +74 -0
- package/src/components/Timeline.tsx +60 -0
- package/src/components/Tree.module.scss +98 -0
- package/src/components/Tree.tsx +185 -0
- package/src/components/VirtualList.module.scss +60 -0
- package/src/components/VirtualList.tsx +251 -0
- package/src/components/app-shell-view.ts +4 -1
- package/src/components/bar-chart-view.ts +121 -39
- package/src/components/board-view.ts +25 -0
- package/src/components/bottom-nav-view.ts +18 -0
- package/src/components/calendar-view.ts +226 -0
- package/src/components/chart-checks.ts +59 -0
- package/src/components/chart-frame-view.ts +33 -2
- package/src/components/chart-reference-view.ts +41 -0
- package/src/components/command-palette-view.ts +47 -6
- package/src/components/confirm-dialog-view.ts +47 -0
- package/src/components/data-table-rows.ts +93 -0
- package/src/components/date-picker-view.ts +38 -0
- package/src/components/date-range-view.ts +46 -0
- package/src/components/diff-view-view.ts +85 -0
- package/src/components/drawer-gesture-view.ts +33 -0
- package/src/components/gauge-view.ts +29 -0
- package/src/components/heat-strip-view.ts +68 -0
- package/src/components/json-view-view.ts +54 -0
- package/src/components/line-chart-view.ts +87 -25
- package/src/components/log-viewer-view.ts +51 -0
- package/src/components/markdown-nodes.tsx +150 -0
- package/src/components/nav-list-view.ts +37 -0
- package/src/components/pull-refresh-view.ts +93 -0
- package/src/components/rank-options.ts +61 -0
- package/src/components/searchable-select-keys.ts +118 -0
- package/src/components/searchable-select-view.ts +187 -0
- package/src/components/slider-view.ts +47 -0
- package/src/components/sort-pointer.ts +141 -0
- package/src/components/sort-surface.tsx +231 -0
- package/src/components/sparkline-view.ts +74 -14
- package/src/components/split-pane-view.ts +69 -0
- package/src/components/stat-delta.ts +54 -11
- package/src/components/stepper-view.ts +38 -0
- package/src/components/thunk.ts +9 -0
- package/src/components/timeline-view.ts +29 -0
- package/src/components/tree-node.tsx +133 -0
- package/src/components/tree-view.ts +185 -0
- package/src/components/use-command-palette.ts +153 -0
- package/src/components/virtual-list-anchor.ts +35 -0
- package/src/components/virtual-list-observer.ts +58 -0
- package/src/components/virtual-list-view.ts +145 -0
- package/src/content/ansi.ts +144 -0
- package/src/content/heading-slug.ts +32 -0
- package/src/content/highlight.ts +129 -0
- package/src/content/languages/all.ts +21 -0
- package/src/content/languages/diff.ts +14 -0
- package/src/content/languages/html.ts +16 -0
- package/src/content/languages/json.ts +15 -0
- package/src/content/languages/shell.ts +20 -0
- package/src/content/languages/sql.ts +27 -0
- package/src/content/languages/ts.ts +26 -0
- package/src/content/languages/yaml.ts +18 -0
- package/src/content/line-diff.ts +191 -0
- package/src/content/markdown-inline.ts +368 -0
- package/src/content/markdown-parse.ts +267 -0
- package/src/content/markdown-tree.ts +58 -0
- package/src/content/unified-patch.ts +53 -0
- package/src/drag/pointer-drag.ts +185 -0
- package/src/drag/sort-controller.ts +196 -0
- package/src/drag/sort-view.ts +201 -0
- package/src/errors.ts +1 -1
- package/src/form/field-path.ts +7 -33
- package/src/form/form-binding.ts +5 -8
- package/src/form/form-issue.ts +30 -36
- package/src/form/form-values.ts +3 -2
- package/src/hotkeys-view.ts +150 -0
- package/src/i18n-keys.ts +7 -50
- package/src/index.ts +214 -15
- package/src/list-url.ts +123 -0
- package/src/live-window.ts +23 -0
- package/src/roving.ts +2 -0
- package/src/theme/inert-runtime.ts +4 -0
- package/src/theme/runtime-slot.ts +2 -2
- package/src/theme/solid-adapter.ts +11 -5
- package/src/time-range-url.ts +200 -0
- package/src/tokens/_mixins.scss +1 -1
- package/src/tokens/_touch.scss +30 -1
- package/src/tokens/contrast-pairs.ts +12 -0
- package/src/ui-keys.ts +126 -0
- package/src/use-hotkeys.ts +145 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// A small tokenizer: a language is an ordered list of sticky regex rules, and the text is cut into
|
|
2
|
+
// classed runs whose JOINED text is the input byte for byte. Classes only — the colours are roles,
|
|
3
|
+
// so a theme recolours. Unknown language: one plain run. `defineLanguage` is the mechanism.
|
|
4
|
+
|
|
5
|
+
import { renderCauseValue } from '@ultimat3/core';
|
|
6
|
+
import { UI_ERROR_CODES, UiError } from '../errors';
|
|
7
|
+
|
|
8
|
+
/** The closed set of classes a run can carry; `CodeBlock.module.scss` styles each by role. */
|
|
9
|
+
export const TOKEN_KINDS = [
|
|
10
|
+
'plain',
|
|
11
|
+
'keyword',
|
|
12
|
+
'string',
|
|
13
|
+
'number',
|
|
14
|
+
'comment',
|
|
15
|
+
'type',
|
|
16
|
+
'function',
|
|
17
|
+
'property',
|
|
18
|
+
'tag',
|
|
19
|
+
'attribute',
|
|
20
|
+
'operator',
|
|
21
|
+
'punctuation',
|
|
22
|
+
'inserted',
|
|
23
|
+
'deleted',
|
|
24
|
+
'meta',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type TokenKind = (typeof TOKEN_KINDS)[number];
|
|
28
|
+
|
|
29
|
+
export interface Token {
|
|
30
|
+
readonly kind: TokenKind;
|
|
31
|
+
readonly text: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TokenRule {
|
|
35
|
+
readonly kind: TokenKind;
|
|
36
|
+
readonly pattern: RegExp;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Language {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Fence info strings that also name it: `typescript`, `js`, `sh`. */
|
|
42
|
+
readonly aliases: readonly string[];
|
|
43
|
+
readonly rules: readonly TokenRule[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Each rule compiled once, sticky, so a match is tried AT a position and never searches ahead. */
|
|
47
|
+
const COMPILED = new WeakMap<Language, readonly { kind: TokenKind; re: RegExp }[]>();
|
|
48
|
+
|
|
49
|
+
function emptyMatchError(language: string, rule: TokenRule): UiError {
|
|
50
|
+
return new UiError({
|
|
51
|
+
code: UI_ERROR_CODES.invalidValue,
|
|
52
|
+
cause: `defineLanguage(${renderCauseValue(language)}): the ${rule.kind} rule ${renderCauseValue(String(rule.pattern))} can match the empty string, which would cut no text and loop`,
|
|
53
|
+
fix: "defineLanguage('mylang', [{ kind: 'keyword', pattern: /\\b(?:if|else)\\b/ }]) # every rule must consume at least one character",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Declares a language. A rule whose pattern can match empty is refused here, where it is written,
|
|
59
|
+
* rather than discovered as a stall on someone's text.
|
|
60
|
+
*/
|
|
61
|
+
export function defineLanguage(
|
|
62
|
+
name: string,
|
|
63
|
+
rules: readonly TokenRule[],
|
|
64
|
+
aliases: readonly string[] = [],
|
|
65
|
+
): Language {
|
|
66
|
+
const compiled = rules.map((rule) => {
|
|
67
|
+
const re = new RegExp(rule.pattern.source, `${rule.pattern.flags.replace(/[gy]/g, '')}y`);
|
|
68
|
+
re.lastIndex = 0;
|
|
69
|
+
if (re.test('')) throw emptyMatchError(name, rule);
|
|
70
|
+
return { kind: rule.kind, re };
|
|
71
|
+
});
|
|
72
|
+
const language: Language = { name, aliases, rules };
|
|
73
|
+
COMPILED.set(language, compiled);
|
|
74
|
+
return language;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The language a fence's info string names, among those the caller passed. */
|
|
78
|
+
export function findLanguage(
|
|
79
|
+
info: string | undefined,
|
|
80
|
+
languages: readonly Language[] | undefined,
|
|
81
|
+
): Language | undefined {
|
|
82
|
+
if (info === undefined || languages === undefined) return undefined;
|
|
83
|
+
const name = info.trim().split(/\s/, 1)[0]?.toLowerCase() ?? '';
|
|
84
|
+
if (name === '') return undefined;
|
|
85
|
+
return languages.find((language) => language.name === name || language.aliases.includes(name));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const WORD = /[\p{L}\p{N}_$]+/uy;
|
|
89
|
+
|
|
90
|
+
/** The runs of `text`. Adjacent runs of one kind are merged; the joined runs equal `text`. */
|
|
91
|
+
export function highlight(text: string, language: Language | undefined): readonly Token[] {
|
|
92
|
+
const rules = language === undefined ? undefined : COMPILED.get(language);
|
|
93
|
+
if (rules === undefined || rules.length === 0)
|
|
94
|
+
return text === '' ? [] : [{ kind: 'plain', text }];
|
|
95
|
+
const out: Token[] = [];
|
|
96
|
+
const push = (kind: TokenKind, piece: string): void => {
|
|
97
|
+
const last = out[out.length - 1];
|
|
98
|
+
if (last !== undefined && last.kind === kind)
|
|
99
|
+
out[out.length - 1] = { kind, text: last.text + piece };
|
|
100
|
+
else out.push({ kind, text: piece });
|
|
101
|
+
};
|
|
102
|
+
let at = 0;
|
|
103
|
+
while (at < text.length) {
|
|
104
|
+
let matched = false;
|
|
105
|
+
for (const rule of rules) {
|
|
106
|
+
rule.re.lastIndex = at;
|
|
107
|
+
const found = rule.re.exec(text);
|
|
108
|
+
// Zero-width is refused at definition, but a lookahead can still match empty mid-text.
|
|
109
|
+
if (found === null || found[0].length === 0) continue;
|
|
110
|
+
push(rule.kind, found[0]);
|
|
111
|
+
at += found[0].length;
|
|
112
|
+
matched = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (matched) continue;
|
|
116
|
+
// A whole word at once, so an identifier is never split for a keyword rule to bite its tail.
|
|
117
|
+
WORD.lastIndex = at;
|
|
118
|
+
const word = WORD.exec(text);
|
|
119
|
+
const piece =
|
|
120
|
+
word === null
|
|
121
|
+
? (text.codePointAt(at) ?? 0) > 0xffff
|
|
122
|
+
? text.slice(at, at + 2)
|
|
123
|
+
: (text[at] ?? '')
|
|
124
|
+
: word[0];
|
|
125
|
+
push('plain', piece);
|
|
126
|
+
at += piece.length;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Every built-in language, for a SERVER page where bytes cost nothing. An island passes only the
|
|
2
|
+
// languages it shows: importing this module from one retains all seven.
|
|
3
|
+
|
|
4
|
+
import type { Language } from '../highlight';
|
|
5
|
+
import { diffLanguage } from './diff';
|
|
6
|
+
import { htmlLanguage } from './html';
|
|
7
|
+
import { jsonLanguage } from './json';
|
|
8
|
+
import { shellLanguage } from './shell';
|
|
9
|
+
import { sqlLanguage } from './sql';
|
|
10
|
+
import { tsLanguage } from './ts';
|
|
11
|
+
import { yamlLanguage } from './yaml';
|
|
12
|
+
|
|
13
|
+
export const allLanguages: readonly Language[] = [
|
|
14
|
+
tsLanguage,
|
|
15
|
+
jsonLanguage,
|
|
16
|
+
shellLanguage,
|
|
17
|
+
sqlLanguage,
|
|
18
|
+
htmlLanguage,
|
|
19
|
+
yamlLanguage,
|
|
20
|
+
diffLanguage,
|
|
21
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// A unified diff as text: file headers, hunk headers, and whole added and removed lines.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const diffLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'diff',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'meta', pattern: /^(?:diff |index |--- |\+\+\+ )[^\n]*/m },
|
|
9
|
+
{ kind: 'keyword', pattern: /^@@[^\n]*/m },
|
|
10
|
+
{ kind: 'inserted', pattern: /^\+[^\n]*/m },
|
|
11
|
+
{ kind: 'deleted', pattern: /^-[^\n]*/m },
|
|
12
|
+
],
|
|
13
|
+
['patch', 'udiff'],
|
|
14
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// HTML and XML markup: comments, tags, attribute names and values. Text between tags is plain.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const htmlLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'html',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'comment', pattern: /<!--[\s\S]*?(?:-->|$)/ },
|
|
9
|
+
{ kind: 'meta', pattern: /<!doctype[^>\n]*>?/i },
|
|
10
|
+
{ kind: 'tag', pattern: /<\/?[a-z][\w:-]*|\/?>/i },
|
|
11
|
+
{ kind: 'string', pattern: /"[^"\n]*"?|'[^'\n]*'?/ },
|
|
12
|
+
{ kind: 'attribute', pattern: /\b[a-z_:][\w:.-]*(?==)/i },
|
|
13
|
+
{ kind: 'operator', pattern: /=/ },
|
|
14
|
+
],
|
|
15
|
+
['xml', 'svg', 'xhtml'],
|
|
16
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// JSON: a key is a string followed by a colon; everything else is a value or punctuation.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const jsonLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'json',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'property', pattern: /"(?:[^"\\\n]|\\.)*"(?=\s*:)/ },
|
|
9
|
+
{ kind: 'string', pattern: /"(?:[^"\\\n]|\\.)*"?/ },
|
|
10
|
+
{ kind: 'number', pattern: /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i },
|
|
11
|
+
{ kind: 'keyword', pattern: /\b(?:true|false|null)\b/ },
|
|
12
|
+
{ kind: 'punctuation', pattern: /[{}[\],:]/ },
|
|
13
|
+
],
|
|
14
|
+
['jsonc', 'json5'],
|
|
15
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// POSIX shell: comments, quoted strings, variables, the usual keywords and a command's flags.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const shellLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'shell',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'comment', pattern: /(?<![^\s;|&])#[^\n]*/ },
|
|
9
|
+
{ kind: 'string', pattern: /"(?:[^"\\]|\\[\s\S])*"?|'[^']*'?/ },
|
|
10
|
+
{ kind: 'property', pattern: /\$(?:\{[^}\n]*\}?|\w+|[@*#?$!0-9-])/ },
|
|
11
|
+
{
|
|
12
|
+
kind: 'keyword',
|
|
13
|
+
pattern:
|
|
14
|
+
/\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|return|export|local|set|unset)\b/,
|
|
15
|
+
},
|
|
16
|
+
{ kind: 'attribute', pattern: /(?<![\w-])--?[\w][\w-]*/ },
|
|
17
|
+
{ kind: 'operator', pattern: /&&|\|\||[|&;<>]+/ },
|
|
18
|
+
],
|
|
19
|
+
['sh', 'bash', 'zsh', 'console', 'shellsession'],
|
|
20
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SQL: comments, strings, quoted identifiers and the keywords Postgres reads most.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const sqlLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'sql',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'comment', pattern: /--[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/ },
|
|
9
|
+
{ kind: 'string', pattern: /'(?:[^']|'')*'?/ },
|
|
10
|
+
{ kind: 'property', pattern: /"(?:[^"]|"")*"?/ },
|
|
11
|
+
{ kind: 'number', pattern: /\b\d+(?:\.\d+)?\b/ },
|
|
12
|
+
{
|
|
13
|
+
kind: 'keyword',
|
|
14
|
+
pattern:
|
|
15
|
+
/\b(?:select|from|where|and|or|not|insert|into|values|update|set|delete|create|alter|drop|table|index|view|on|join|left|right|inner|outer|cross|group|by|order|having|limit|offset|as|distinct|union|all|case|when|then|else|end|null|is|in|exists|between|like|ilike|primary|key|references|foreign|unique|default|constraint|returning|with|begin|commit|rollback|asc|desc|true|false|check|cascade)\b/i,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
kind: 'type',
|
|
19
|
+
pattern:
|
|
20
|
+
/\b(?:int|integer|bigint|smallint|text|varchar|char|boolean|bool|uuid|jsonb?|timestamptz?|date|numeric|serial|bigserial|bytea)\b/i,
|
|
21
|
+
},
|
|
22
|
+
{ kind: 'function', pattern: /\b[a-z_]\w*(?=\()/i },
|
|
23
|
+
{ kind: 'operator', pattern: /[=<>!]=?|<>|::|\|\||[+\-*/%]/ },
|
|
24
|
+
{ kind: 'punctuation', pattern: /[(),;.]/ },
|
|
25
|
+
],
|
|
26
|
+
['postgres', 'postgresql', 'psql', 'pgsql'],
|
|
27
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// TypeScript and JavaScript: comments, strings and templates (line-bounded but for templates and
|
|
2
|
+
// block comments, which run to the end of an unterminated input rather than rescanning it).
|
|
3
|
+
|
|
4
|
+
import { defineLanguage } from '../highlight';
|
|
5
|
+
|
|
6
|
+
export const tsLanguage = /* @__PURE__ */ defineLanguage(
|
|
7
|
+
'ts',
|
|
8
|
+
[
|
|
9
|
+
{ kind: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/ },
|
|
10
|
+
{
|
|
11
|
+
kind: 'string',
|
|
12
|
+
pattern: /"(?:[^"\\\n]|\\.)*"?|'(?:[^'\\\n]|\\.)*'?|`(?:[^`\\]|\\[\s\S])*`?/,
|
|
13
|
+
},
|
|
14
|
+
{ kind: 'number', pattern: /\b(?:0[xob][\da-f_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-]?\d+)?)n?\b/i },
|
|
15
|
+
{
|
|
16
|
+
kind: 'keyword',
|
|
17
|
+
pattern:
|
|
18
|
+
/\b(?:as|async|await|break|case|catch|class|const|continue|default|delete|do|else|enum|export|extends|false|finally|for|from|function|if|implements|import|in|instanceof|interface|let|new|null|of|private|protected|public|readonly|return|satisfies|static|super|switch|this|throw|true|try|type|typeof|undefined|var|void|while|yield)\b/,
|
|
19
|
+
},
|
|
20
|
+
{ kind: 'type', pattern: /\b[A-Z][\w$]*\b/ },
|
|
21
|
+
{ kind: 'function', pattern: /\b[a-z_$][\w$]*(?=\()/ },
|
|
22
|
+
{ kind: 'operator', pattern: /=>|[=!<>]=?=?|&&|\|\||\?\?|[+\-*/%&|^~?]=?/ },
|
|
23
|
+
{ kind: 'punctuation', pattern: /[{}()[\];,.:]/ },
|
|
24
|
+
],
|
|
25
|
+
['typescript', 'tsx', 'js', 'javascript', 'jsx', 'mjs', 'cjs'],
|
|
26
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// YAML: comments, keys before a colon, quoted scalars, anchors, document markers and list dashes.
|
|
2
|
+
|
|
3
|
+
import { defineLanguage } from '../highlight';
|
|
4
|
+
|
|
5
|
+
export const yamlLanguage = /* @__PURE__ */ defineLanguage(
|
|
6
|
+
'yaml',
|
|
7
|
+
[
|
|
8
|
+
{ kind: 'comment', pattern: /(?<![^\s])#[^\n]*/ },
|
|
9
|
+
{ kind: 'meta', pattern: /^(?:---|\.\.\.)(?=\s|$)/m },
|
|
10
|
+
{ kind: 'property', pattern: /[\w.-][\w .-]*(?=:(?:\s|$))/ },
|
|
11
|
+
{ kind: 'string', pattern: /"(?:[^"\\\n]|\\.)*"?|'(?:[^'\n]|'')*'?/ },
|
|
12
|
+
{ kind: 'keyword', pattern: /\b(?:true|false|null|yes|no|on|off)\b/i },
|
|
13
|
+
{ kind: 'number', pattern: /-?\b\d+(?:\.\d+)?\b/ },
|
|
14
|
+
{ kind: 'attribute', pattern: /[&*][\w-]+|![\w!/-]*/ },
|
|
15
|
+
{ kind: 'punctuation', pattern: /[:\-?[\]{},|>]/ },
|
|
16
|
+
],
|
|
17
|
+
['yml'],
|
|
18
|
+
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Myers' diff over lines, then word marks inside each changed pair. Bounded on hostile input: the
|
|
2
|
+
// common ends are trimmed first, and past `maxEdits` the middle is reported as removed-then-added
|
|
3
|
+
// rather than searched quadratically. The rows are what `DiffView` and a unified patch share.
|
|
4
|
+
|
|
5
|
+
/** One row of a diff, numbered on the side(s) it exists on. `marks` are changed char ranges. */
|
|
6
|
+
export type DiffRow =
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: 'context' | 'add' | 'del';
|
|
9
|
+
readonly old?: number | undefined;
|
|
10
|
+
readonly new?: number | undefined;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly marks?: readonly (readonly [number, number])[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
| { readonly kind: 'hunk' | 'file'; readonly text: string }
|
|
15
|
+
| { readonly kind: 'fold'; readonly count: number };
|
|
16
|
+
|
|
17
|
+
export type DiffOp = 'equal' | 'add' | 'del';
|
|
18
|
+
|
|
19
|
+
/** Past this many edits the search stops; the rest is a plain replace, in linear time. */
|
|
20
|
+
export const DIFF_MAX_EDITS = 1_000;
|
|
21
|
+
|
|
22
|
+
/** The edit script turning `a` into `b`, as one op per element, Myers O((N+M)·D) within the cap. */
|
|
23
|
+
export function diffOps<T>(a: readonly T[], b: readonly T[], maxEdits = DIFF_MAX_EDITS): DiffOp[] {
|
|
24
|
+
let head = 0;
|
|
25
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
|
|
26
|
+
let tail = 0;
|
|
27
|
+
while (
|
|
28
|
+
tail < a.length - head &&
|
|
29
|
+
tail < b.length - head &&
|
|
30
|
+
a[a.length - 1 - tail] === b[b.length - 1 - tail]
|
|
31
|
+
)
|
|
32
|
+
tail += 1;
|
|
33
|
+
const x0 = a.slice(head, a.length - tail);
|
|
34
|
+
const y0 = b.slice(head, b.length - tail);
|
|
35
|
+
const middle = myers(x0, y0, maxEdits) ?? [
|
|
36
|
+
...x0.map((): DiffOp => 'del'),
|
|
37
|
+
...y0.map((): DiffOp => 'add'),
|
|
38
|
+
];
|
|
39
|
+
return [
|
|
40
|
+
...new Array<DiffOp>(head).fill('equal'),
|
|
41
|
+
...middle,
|
|
42
|
+
...new Array<DiffOp>(tail).fill('equal'),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function myers<T>(a: readonly T[], b: readonly T[], maxEdits: number): DiffOp[] | null {
|
|
47
|
+
const n = a.length;
|
|
48
|
+
const m = b.length;
|
|
49
|
+
const max = Math.min(n + m, maxEdits);
|
|
50
|
+
const offset = max + 1;
|
|
51
|
+
let v = new Int32Array(2 * max + 3);
|
|
52
|
+
const trace: Int32Array[] = [];
|
|
53
|
+
for (let d = 0; d <= max; d += 1) {
|
|
54
|
+
trace.push(v.slice());
|
|
55
|
+
const next = v.slice();
|
|
56
|
+
for (let k = -d; k <= d; k += 2) {
|
|
57
|
+
const down = k === -d || (k !== d && (v[offset + k - 1] ?? 0) < (v[offset + k + 1] ?? 0));
|
|
58
|
+
let x = down ? (v[offset + k + 1] ?? 0) : (v[offset + k - 1] ?? 0) + 1;
|
|
59
|
+
let y = x - k;
|
|
60
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
61
|
+
x += 1;
|
|
62
|
+
y += 1;
|
|
63
|
+
}
|
|
64
|
+
next[offset + k] = x;
|
|
65
|
+
if (x >= n && y >= m) return backtrack(trace, next, n, m, offset);
|
|
66
|
+
}
|
|
67
|
+
v = next;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function backtrack(
|
|
73
|
+
trace: readonly Int32Array[],
|
|
74
|
+
last: Int32Array,
|
|
75
|
+
n: number,
|
|
76
|
+
m: number,
|
|
77
|
+
offset: number,
|
|
78
|
+
): DiffOp[] {
|
|
79
|
+
const ops: DiffOp[] = [];
|
|
80
|
+
let x = n;
|
|
81
|
+
let y = m;
|
|
82
|
+
for (let d = trace.length - 1; d >= 0; d -= 1) {
|
|
83
|
+
const v = trace[d] ?? last;
|
|
84
|
+
const k = x - y;
|
|
85
|
+
const down = k === -d || (k !== d && (v[offset + k - 1] ?? 0) < (v[offset + k + 1] ?? 0));
|
|
86
|
+
const prevK = down ? k + 1 : k - 1;
|
|
87
|
+
const prevX = d === 0 ? 0 : (v[offset + prevK] ?? 0);
|
|
88
|
+
const prevY = prevX - prevK;
|
|
89
|
+
while (x > prevX && y > prevY) {
|
|
90
|
+
ops.push('equal');
|
|
91
|
+
x -= 1;
|
|
92
|
+
y -= 1;
|
|
93
|
+
}
|
|
94
|
+
if (d > 0) ops.push(down ? 'add' : 'del');
|
|
95
|
+
x = prevX;
|
|
96
|
+
y = prevY;
|
|
97
|
+
}
|
|
98
|
+
return ops.reverse();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const WORDS = /\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu;
|
|
102
|
+
/** A changed pair longer than this many words gets no marks: the line itself is the mark. */
|
|
103
|
+
const MARK_MAX_WORDS = 400;
|
|
104
|
+
|
|
105
|
+
/** Changed character ranges in a removed line and its added partner. */
|
|
106
|
+
export function wordMarks(
|
|
107
|
+
before: string,
|
|
108
|
+
after: string,
|
|
109
|
+
): readonly [readonly (readonly [number, number])[], readonly (readonly [number, number])[]] {
|
|
110
|
+
const a = before.match(WORDS) ?? [];
|
|
111
|
+
const b = after.match(WORDS) ?? [];
|
|
112
|
+
if (a.length > MARK_MAX_WORDS || b.length > MARK_MAX_WORDS) return [[], []];
|
|
113
|
+
const ops = diffOps(a, b, MARK_MAX_WORDS);
|
|
114
|
+
const left: [number, number][] = [];
|
|
115
|
+
const right: [number, number][] = [];
|
|
116
|
+
let i = 0;
|
|
117
|
+
let j = 0;
|
|
118
|
+
let at = 0;
|
|
119
|
+
let bt = 0;
|
|
120
|
+
const mark = (into: [number, number][], from: number, to: number): void => {
|
|
121
|
+
const prev = into[into.length - 1];
|
|
122
|
+
if (prev !== undefined && prev[1] === from) prev[1] = to;
|
|
123
|
+
else into.push([from, to]);
|
|
124
|
+
};
|
|
125
|
+
for (const op of ops) {
|
|
126
|
+
if (op !== 'add') {
|
|
127
|
+
const len = (a[i] ?? '').length;
|
|
128
|
+
if (op === 'del') mark(left, at, at + len);
|
|
129
|
+
at += len;
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
if (op !== 'del') {
|
|
133
|
+
const len = (b[j] ?? '').length;
|
|
134
|
+
if (op === 'add') mark(right, bt, bt + len);
|
|
135
|
+
bt += len;
|
|
136
|
+
j += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [left, right];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Numbered rows from two texts, word-marked where a removed run meets an added one. */
|
|
143
|
+
export function lineDiff(before: string, after: string, maxEdits = DIFF_MAX_EDITS): DiffRow[] {
|
|
144
|
+
const a = before === '' ? [] : before.replace(/\r\n?/g, '\n').split('\n');
|
|
145
|
+
const b = after === '' ? [] : after.replace(/\r\n?/g, '\n').split('\n');
|
|
146
|
+
const rows: DiffRow[] = [];
|
|
147
|
+
let i = 0;
|
|
148
|
+
let j = 0;
|
|
149
|
+
for (const op of diffOps(a, b, maxEdits)) {
|
|
150
|
+
if (op === 'equal') {
|
|
151
|
+
rows.push({ kind: 'context', old: i + 1, new: j + 1, text: a[i] ?? '' });
|
|
152
|
+
i += 1;
|
|
153
|
+
j += 1;
|
|
154
|
+
} else if (op === 'del') {
|
|
155
|
+
rows.push({ kind: 'del', old: i + 1, text: a[i] ?? '' });
|
|
156
|
+
i += 1;
|
|
157
|
+
} else {
|
|
158
|
+
rows.push({ kind: 'add', new: j + 1, text: b[j] ?? '' });
|
|
159
|
+
j += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return markPairs(rows);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Pairs each run of removed rows with the added run after it, index by index, and marks words. */
|
|
166
|
+
export function markPairs(rows: readonly DiffRow[]): DiffRow[] {
|
|
167
|
+
const out = [...rows];
|
|
168
|
+
for (let r = 0; r < out.length; ) {
|
|
169
|
+
let dels = r;
|
|
170
|
+
while (out[dels]?.kind === 'del') dels += 1;
|
|
171
|
+
let adds = dels;
|
|
172
|
+
while (out[adds]?.kind === 'add') adds += 1;
|
|
173
|
+
const pairs = Math.min(dels - r, adds - dels);
|
|
174
|
+
for (let p = 0; p < pairs; p += 1) {
|
|
175
|
+
const del = out[r + p];
|
|
176
|
+
const add = out[dels + p];
|
|
177
|
+
if (del === undefined || add === undefined || !('text' in del) || !('text' in add)) continue;
|
|
178
|
+
const [left, right] = wordMarks(del.text, add.text);
|
|
179
|
+
out[r + p] = { ...del, marks: left } as DiffRow;
|
|
180
|
+
out[dels + p] = { ...add, marks: right } as DiffRow;
|
|
181
|
+
}
|
|
182
|
+
r = adds === r ? r + 1 : adds;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** At most `maxLines` rows, then one fold saying how many were left out. */
|
|
188
|
+
export function foldRows(rows: readonly DiffRow[], maxLines: number): readonly DiffRow[] {
|
|
189
|
+
if (rows.length <= maxLines) return rows;
|
|
190
|
+
return [...rows.slice(0, maxLines), { kind: 'fold', count: rows.length - maxLines }];
|
|
191
|
+
}
|