@synclineapi/editor 4.0.3 → 4.0.4

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 CHANGED
@@ -22,6 +22,7 @@ Ships as both an **ES module** and **UMD bundle**, runs entirely inside a **Shad
22
22
  - [Editing](#editing)
23
23
  - [Features](#features-config)
24
24
  - [Syntax & Autocomplete](#syntax--autocomplete-customisation)
25
+ - [Custom Syntax Highlighter — provideTokens](#custom-syntax-highlighter--providetokens)
25
26
  - [Token Colors](#token-colors-config)
26
27
  - [Theme](#theme-config)
27
28
  - [Callbacks](#callbacks)
@@ -61,6 +62,7 @@ Ships as both an **ES module** and **UMD bundle**, runs entirely inside a **Shad
61
62
  - [Dynamic Completion Provider](#dynamic-completion-provider)
62
63
  - [provideCompletions](#providecompletions)
63
64
  - [CompletionContext](#completioncontext)
65
+ - [Custom Syntax Highlighter — provideTokens](#custom-syntax-highlighter--providetokens)
64
66
  - [Events & Callbacks](#events--callbacks)
65
67
  - [Advanced Features](#advanced-features)
66
68
  - [Multi-cursor](#multi-cursor)
@@ -89,6 +91,7 @@ Ships as both an **ES module** and **UMD bundle**, runs entirely inside a **Shad
89
91
  - [Keyboard Shortcuts](#keyboard-shortcuts)
90
92
  - [Recipes](#recipes)
91
93
  - [Monaco-style Editor Embed](#monaco-style-editor-embed)
94
+ - [Markdown Editor](#markdown-editor)
92
95
  - [DSL / SQL Editor](#dsl--sql-editor)
93
96
  - [Read-Only Code Viewer](#read-only-code-viewer)
94
97
  - [Custom Theme from Scratch](#custom-theme-from-scratch-recipe)
@@ -109,6 +112,7 @@ Ships as both an **ES module** and **UMD bundle**, runs entirely inside a **Shad
109
112
  | **Shadow DOM** | Fully encapsulated styles — no CSS leakage in either direction |
110
113
  | **Dual build** | ES module + UMD bundle, full TypeScript declarations |
111
114
  | **Syntax highlighting** | TypeScript, JavaScript, CSS, JSON, Markdown — nine token classes |
115
+ | **`provideTokens` callback** | Fully override the built-in tokeniser for any language — pass `(line, language) => TokenSegment[]` |
112
116
  | **Token color overrides** | Override individual token colours on top of any theme without replacing it |
113
117
  | **6 built-in themes** | VR Dark, VS Code Dark+, Monokai, Dracula, GitHub Light, Solarized Light |
114
118
  | **Custom themes** | Full `ThemeDefinition` API — every CSS variable exposed |
@@ -274,6 +278,7 @@ Default `autoClosePairs`:
274
278
  | `replaceBuiltins` | `boolean` | `false` | When `true`, `completions` replaces the built-in language keywords/types entirely. |
275
279
  | `provideCompletions` | `(ctx: CompletionContext) => CompletionItem[] \| null` | `undefined` | Dynamic callback — called on every popup open; return `null` to fall through to defaults. |
276
280
  | `provideHover` | `(ctx: HoverContext) => HoverDoc \| null` | `undefined` | Dynamic hover callback — return a `HoverDoc` to show a tooltip for any word not covered by built-in docs or the `completions` array. |
281
+ | `provideTokens` | `(line: string, language: string) => TokenSegment[]` | `undefined` | Fully override syntax tokenisation. Called for every visible line on every render. Return an array of `TokenSegment` objects. When set, the built-in tokeniser is **never called** — the callback owns all highlighting for every language. See [Custom Syntax Highlighter](#custom-syntax-highlighter--providetokens). |
277
282
  | `maxCompletions` | `number` | `14` | Maximum items shown in the autocomplete popup. |
278
283
 
279
284
  ### Token Colors Config
@@ -1039,6 +1044,120 @@ createEditor(container, { emmet: false }); // disable
1039
1044
 
1040
1045
  ---
1041
1046
 
1047
+ ## Custom Syntax Highlighter — `provideTokens`
1048
+
1049
+ When the built-in tokeniser doesn't cover your language, supply a `provideTokens` callback and own syntax highlighting entirely:
1050
+
1051
+ ```ts
1052
+ provideTokens?: (line: string, language: string) => TokenSegment[]
1053
+ ```
1054
+
1055
+ - Called for **every visible line** on every render pass.
1056
+ - When set, the built-in tokeniser is **never called** — your callback is the sole source of token segments.
1057
+ - Any line not fully covered by your segments renders as plain text for those characters.
1058
+ - Runs synchronously on the render thread — keep it fast (no I/O, no heavy regex backtracking).
1059
+ - The `language` argument is the active `EditorConfig.language` string — use it when you register one callback for multiple languages.
1060
+
1061
+ ### `TokenSegment` shape
1062
+
1063
+ ```ts
1064
+ interface TokenSegment {
1065
+ cls: TokenClass; // which colour to apply — see table below
1066
+ start: number; // start character index in the line (inclusive)
1067
+ end: number; // end character index in the line (exclusive)
1068
+ }
1069
+ ```
1070
+
1071
+ Segments may overlap; later segments in the array take priority. Gaps between segments render as plain text (`--tok-op` colour).
1072
+
1073
+ ### `TokenClass` values
1074
+
1075
+ | `cls` | CSS variable | Typical use |
1076
+ |---|---|---|
1077
+ | `'kw'` | `--tok-kw` | Keywords, control flow |
1078
+ | `'str'` | `--tok-str` | String and template literals |
1079
+ | `'cmt'` | `--tok-cmt` | Comments |
1080
+ | `'fn'` | `--tok-fn` | Function names, calls |
1081
+ | `'num'` | `--tok-num` | Numeric literals |
1082
+ | `'cls'` | `--tok-cls` | Class / type names |
1083
+ | `'op'` | `--tok-op` | Operators, punctuation, plain text |
1084
+ | `'typ'` | `--tok-typ` | Type annotations |
1085
+ | `'dec'` | `--tok-dec` | Decorators / annotations |
1086
+
1087
+ ### Markdown example
1088
+
1089
+ The built-in `'markdown'` language produces no highlighting — it runs the generic path. Use `provideTokens` to add full Markdown highlighting:
1090
+
1091
+ ```ts
1092
+ import { createEditor } from 'syncline-editor';
1093
+ import type { TokenSegment } from 'syncline-editor';
1094
+
1095
+ createEditor(container, {
1096
+ language: 'markdown',
1097
+ provideTokens: (line): TokenSegment[] => {
1098
+ const segs: TokenSegment[] = [];
1099
+
1100
+ // ATX headings # / ## / … → keyword colour
1101
+ if (/^#{1,6}(\s|$)/.test(line)) {
1102
+ segs.push({ cls: 'kw', start: 0, end: line.length });
1103
+ return segs;
1104
+ }
1105
+
1106
+ // Fenced code block delimiter ``` or ~~~
1107
+ if (/^(`{3,}|~{3,})/.test(line)) {
1108
+ segs.push({ cls: 'cmt', start: 0, end: line.length });
1109
+ return segs;
1110
+ }
1111
+
1112
+ // Blockquote > …
1113
+ if (/^>/.test(line)) {
1114
+ segs.push({ cls: 'cmt', start: 0, end: line.length });
1115
+ return segs;
1116
+ }
1117
+
1118
+ // Inline patterns — scan with regex
1119
+ const inlineRules: [RegExp, TokenSegment['cls']][] = [
1120
+ [/`[^`]+`/g, 'str'], // inline code
1121
+ [/\*\*[^*]+\*\*/g, 'kw'], // bold
1122
+ [/\*[^*]+\*/g, 'typ'], // italic
1123
+ [/~~[^~]+~~/g, 'cmt'], // strikethrough
1124
+ [/!?\[[^\]]*\]\([^)]*\)/g, 'fn'], // links / images
1125
+ [/^([-*+]|\d+\.)\s/, 'dec'], // list marker
1126
+ ];
1127
+
1128
+ for (const [re, cls] of inlineRules) {
1129
+ let m: RegExpExecArray | null;
1130
+ const g = new RegExp(re.source, 'g');
1131
+ while ((m = g.exec(line)) !== null) {
1132
+ segs.push({ cls, start: m.index, end: m.index + m[0].length });
1133
+ }
1134
+ }
1135
+
1136
+ return segs;
1137
+ },
1138
+ });
1139
+ ```
1140
+
1141
+ ### Switching the callback at runtime
1142
+
1143
+ `updateConfig` clears the token cache before applying the new callback, so the next render is always fresh:
1144
+
1145
+ ```ts
1146
+ // Switch from Markdown highlighting to a plain-text view
1147
+ editor.updateConfig({ provideTokens: undefined });
1148
+
1149
+ // Swap to a different highlighter entirely
1150
+ editor.updateConfig({ provideTokens: myNewHighlighter });
1151
+ ```
1152
+
1153
+ ### Performance tips
1154
+
1155
+ - Avoid constructing new `RegExp` objects inside the callback on every call — define them outside and reuse.
1156
+ - Return an **empty array** `[]` for lines you intentionally want as plain text rather than pushing an `op` segment for the whole line.
1157
+ - For large files, keep per-line work to O(line-length) — the callback is invoked once per visible row per frame.
1158
+
1159
+ ---
1160
+
1042
1161
  ## Dynamic Completion Provider
1043
1162
 
1044
1163
  Use `provideCompletions` for fully context-aware completions that change based on the cursor position, current prefix, or document content. When this callback returns a non-null array, it **completely overrides** all other completion sources.
@@ -1407,15 +1526,17 @@ createEditor(container, {
1407
1526
  | `Ctrl / Cmd + Z` | Undo | `undoBatchMs`, `maxUndoHistory` |
1408
1527
  | `Ctrl / Cmd + Y` or `Ctrl + Shift + Z` | Redo | — |
1409
1528
  | `Ctrl / Cmd + A` | Select all | — |
1410
- | `Ctrl / Cmd + C` | Copy | — |
1411
- | `Ctrl / Cmd + X` | Cut | `readOnly` |
1412
- | `Ctrl / Cmd + V` | Paste | `readOnly` |
1529
+ | `Ctrl / Cmd + C` | Copy selection; **with no selection copies the entire current line** (pasting inserts it above the cursor line) | — |
1530
+ | `Ctrl / Cmd + X` | Cut selection; **with no selection cuts the entire current line** | `readOnly` |
1531
+ | `Ctrl / Cmd + V` | Paste; if clipboard holds a whole-line copy, inserts the line above the cursor line and shifts cursor down | `readOnly` |
1413
1532
  | `Ctrl / Cmd + F` | Open find bar | `find` |
1414
1533
  | `Ctrl / Cmd + H` | Open find + replace | `findReplace` |
1415
1534
  | `Ctrl / Cmd + G` | Open Go to Line prompt | `goToLine` |
1416
- | `Ctrl / Cmd + D` | Select next occurrence | `multiCursor` |
1535
+ | `Ctrl / Cmd + D` | Select next occurrence (first press selects current word; each subsequent press adds next occurrence) | `multiCursor` |
1417
1536
  | `Ctrl / Cmd + Shift + D` | Duplicate line | `readOnly` |
1418
1537
  | `Ctrl / Cmd + K` | Delete line | `readOnly` |
1538
+ | `Cmd + Enter` | Insert new line below cursor (without moving to end of line) | `readOnly` |
1539
+ | `Cmd + Shift + Enter` | Insert new line above cursor | `readOnly` |
1419
1540
  | `Ctrl / Cmd + /` | Toggle line comment | `lineCommentToken`, `readOnly` |
1420
1541
  | `Alt / Option + Z` | Toggle word wrap | — |
1421
1542
  | `Alt / Option + ↑` | Move current line (or selected block) up | `readOnly` |
@@ -1446,12 +1567,84 @@ createEditor(container, {
1446
1567
 
1447
1568
  All mutating shortcuts are silently blocked when `readOnly: true`. Navigation, selection, and `Ctrl+C` always work.
1448
1569
 
1570
+ > **Linewise copy/cut/paste** (when there is no active selection) follows VS Code exactly: `Ctrl/Cmd+C` copies the whole line including its newline; `Ctrl/Cmd+X` cuts it; `Ctrl/Cmd+V` inserts the line *above* the current line and moves the cursor to the original line — so the document is shifted down by one but the cursor text is unchanged.
1571
+
1449
1572
  ---
1450
1573
 
1451
1574
  ## Recipes
1452
1575
 
1453
1576
  Real-world patterns you can copy and adapt.
1454
1577
 
1578
+ ### Markdown Editor
1579
+
1580
+ Full-featured Markdown editor with custom syntax highlighting via `provideTokens`, appropriate language settings, and bracket pairs disabled (Markdown doesn't use them):
1581
+
1582
+ ```ts
1583
+ import { createEditor } from 'syncline-editor';
1584
+ import type { TokenSegment } from 'syncline-editor';
1585
+
1586
+ function markdownTokens(line: string): TokenSegment[] {
1587
+ const segs: TokenSegment[] = [];
1588
+
1589
+ if (/^#{1,6}(\s|$)/.test(line))
1590
+ return [{ cls: 'kw', start: 0, end: line.length }];
1591
+ if (/^(`{3,}|~{3,})/.test(line))
1592
+ return [{ cls: 'cmt', start: 0, end: line.length }];
1593
+ if (/^>/.test(line))
1594
+ return [{ cls: 'cmt', start: 0, end: line.length }];
1595
+
1596
+ const rules: [RegExp, TokenSegment['cls']][] = [
1597
+ [/`[^`]+`/g, 'str'],
1598
+ [/\*\*[^*]+\*\*/g, 'kw' ],
1599
+ [/\*[^*]+\*/g, 'typ'],
1600
+ [/~~[^~]+~~/g, 'cmt'],
1601
+ [/!?\[[^\]]*\]\([^)]*\)/g, 'fn' ],
1602
+ [/^([-*+]|\d+\.)\s/, 'dec'],
1603
+ ];
1604
+
1605
+ for (const [re, cls] of rules) {
1606
+ let m: RegExpExecArray | null;
1607
+ const g = new RegExp(re.source, 'g');
1608
+ while ((m = g.exec(line)) !== null)
1609
+ segs.push({ cls, start: m.index, end: m.index + m[0].length });
1610
+ }
1611
+ return segs;
1612
+ }
1613
+
1614
+ const editor = createEditor(document.getElementById('md-editor')!, {
1615
+ language: 'markdown',
1616
+ theme: 'github-light',
1617
+ provideTokens: markdownTokens,
1618
+ lineCommentToken: '', // no line-comment toggle
1619
+ autoClosePairs: { '(': ')', '[': ']', '`': '`', '*': '*', '_': '_' },
1620
+ bracketMatching: false, // not useful in prose
1621
+ emmet: false,
1622
+ wordWrap: true,
1623
+ wrapColumn: 80,
1624
+ showMinimap: false,
1625
+ tabSize: 2,
1626
+ onChange: (value) => preview.setContent(value),
1627
+ });
1628
+ ```
1629
+
1630
+ ```css
1631
+ #md-editor { width: 100%; height: 100vh; }
1632
+ ```
1633
+
1634
+ **Updating the highlighter without recreating the editor:**
1635
+
1636
+ ```ts
1637
+ // Extend the highlighter at runtime — e.g. add frontmatter support
1638
+ editor.updateConfig({
1639
+ provideTokens: (line) => {
1640
+ if (/^---$/.test(line)) return [{ cls: 'dec', start: 0, end: 3 }];
1641
+ return markdownTokens(line);
1642
+ },
1643
+ });
1644
+ ```
1645
+
1646
+ ---
1647
+
1455
1648
  ### Monaco-style Editor Embed
1456
1649
 
1457
1650
  A feature-complete editor inside a fixed-height container, closely resembling a VS Code embed:
@@ -1676,6 +1869,10 @@ import type {
1676
1869
  // Token colours
1677
1870
  TokenColors,
1678
1871
 
1872
+ // Custom tokeniser
1873
+ TokenSegment,
1874
+ TokenClass,
1875
+
1679
1876
  // Themes
1680
1877
  ThemeDefinition,
1681
1878
  ThemeTokens,
@@ -1747,6 +1944,22 @@ interface CompletionContext {
1747
1944
  }
1748
1945
  ```
1749
1946
 
1947
+ ### `TokenSegment`
1948
+
1949
+ ```ts
1950
+ interface TokenSegment {
1951
+ cls: TokenClass; // which token colour class to apply
1952
+ start: number; // start character index in the line (inclusive)
1953
+ end: number; // end character index in the line (exclusive)
1954
+ }
1955
+ ```
1956
+
1957
+ ### `TokenClass`
1958
+
1959
+ ```ts
1960
+ type TokenClass = 'kw' | 'str' | 'cmt' | 'fn' | 'num' | 'cls' | 'op' | 'typ' | 'dec';
1961
+ ```
1962
+
1750
1963
  ### `TokenColors`
1751
1964
 
1752
1965
  ```ts