@inkbrick/react 0.0.0-stage → 0.1.0-alpha.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.
Files changed (131) hide show
  1. package/dist/FormulaPalette-WFRPRHS5.js +320 -0
  2. package/dist/FormulaPalette-WFRPRHS5.js.map +1 -0
  3. package/dist/chunk-TPUX4C45.js +2309 -0
  4. package/dist/chunk-TPUX4C45.js.map +1 -0
  5. package/dist/index.d.ts +1431 -0
  6. package/dist/index.js +4880 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/mathCss-FLPE6QBM.js +361 -0
  9. package/dist/mathCss-FLPE6QBM.js.map +1 -0
  10. package/package.json +46 -3
  11. package/src/block-handle/BlockHandleController.ts +93 -0
  12. package/src/block-handle/BlockHandleHost.tsx +108 -0
  13. package/src/block-handle/DefaultBlockHandle.tsx +71 -0
  14. package/src/block-handle/DefaultBlockMenu.tsx +114 -0
  15. package/src/block-handle/DropIndicatorHost.tsx +58 -0
  16. package/src/block-handle/actions.tsx +146 -0
  17. package/src/block-handle/blockDrag.ts +215 -0
  18. package/src/block-handle/geometry.ts +64 -0
  19. package/src/block-handle/gripGesture.ts +108 -0
  20. package/src/block-handle/types.ts +65 -0
  21. package/src/block-handle/useBlockDrag.ts +21 -0
  22. package/src/block-handle/useBlockHandles.ts +125 -0
  23. package/src/blocks/BlockChildren.tsx +17 -0
  24. package/src/blocks/BlockProps.ts +5 -0
  25. package/src/blocks/CalloutBlock.tsx +61 -0
  26. package/src/blocks/CodeBlock.tsx +96 -0
  27. package/src/blocks/DividerBlock.tsx +15 -0
  28. package/src/blocks/Editable.tsx +84 -0
  29. package/src/blocks/FormulaBlock.tsx +173 -0
  30. package/src/blocks/HeadingBlock.tsx +22 -0
  31. package/src/blocks/ImageBlock.tsx +362 -0
  32. package/src/blocks/ListBlock.tsx +86 -0
  33. package/src/blocks/NoteBlock.tsx +10 -0
  34. package/src/blocks/PageBlock.tsx +10 -0
  35. package/src/blocks/ParagraphBlock.tsx +20 -0
  36. package/src/blocks/QuoteBlock.tsx +18 -0
  37. package/src/blocks/UnknownBlockView.tsx +27 -0
  38. package/src/blocks/defineBlock.ts +49 -0
  39. package/src/clipboard/html/blocks.ts +379 -0
  40. package/src/clipboard/html/inline.ts +92 -0
  41. package/src/clipboard/html/style.ts +92 -0
  42. package/src/clipboard/html/tags.ts +77 -0
  43. package/src/clipboard/htmlToFragment.ts +41 -0
  44. package/src/clipboard/useClipboard.ts +296 -0
  45. package/src/code/highlight.ts +56 -0
  46. package/src/code/languages.ts +236 -0
  47. package/src/code/scan/css.ts +164 -0
  48. package/src/code/scan/generic.ts +328 -0
  49. package/src/code/scan/html.ts +129 -0
  50. package/src/code/scan/markdown.ts +58 -0
  51. package/src/code/scan/shared.ts +81 -0
  52. package/src/code/scan/yaml.ts +34 -0
  53. package/src/code/types.ts +34 -0
  54. package/src/context/EditorContext.ts +9 -0
  55. package/src/context/EditorProvider.tsx +172 -0
  56. package/src/controllers/adapters.tsx +63 -0
  57. package/src/editing/BlockSelectionController.ts +27 -0
  58. package/src/editing/CaretController.ts +27 -0
  59. package/src/editing/EditorView.ts +108 -0
  60. package/src/editing/ViewportController.ts +63 -0
  61. package/src/editing/beforeInput.ts +276 -0
  62. package/src/editing/deleteAtBoundary.ts +16 -0
  63. package/src/editing/inputActions.ts +109 -0
  64. package/src/editing/renderCode.ts +69 -0
  65. package/src/editing/renderInline.ts +144 -0
  66. package/src/editing/selection.ts +262 -0
  67. package/src/editing/useEditorInput.ts +223 -0
  68. package/src/editing/useSelectionState.ts +46 -0
  69. package/src/formula/FormulaController.ts +51 -0
  70. package/src/formula/FormulaPalette.tsx +79 -0
  71. package/src/formula/FormulaPopover.tsx +181 -0
  72. package/src/formula/LazyFormulaPalette.tsx +42 -0
  73. package/src/formula/actions.ts +138 -0
  74. package/src/formula/snippet.ts +70 -0
  75. package/src/formula/symbols.ts +264 -0
  76. package/src/hooks/useBlock.ts +21 -0
  77. package/src/hooks/useBlockController.ts +30 -0
  78. package/src/hooks/useChildren.ts +16 -0
  79. package/src/hooks/useDocument.ts +16 -0
  80. package/src/hooks/useEditor.ts +13 -0
  81. package/src/hooks/useEditorView.ts +13 -0
  82. package/src/hooks/useHistory.ts +46 -0
  83. package/src/hooks/useReadOnly.ts +21 -0
  84. package/src/hooks/useT.ts +10 -0
  85. package/src/i18n/en.ts +182 -0
  86. package/src/i18n/ru.ts +174 -0
  87. package/src/i18n/translate.ts +77 -0
  88. package/src/icons/ICONS_LICENSE.md +50 -0
  89. package/src/icons/Icon.tsx +41 -0
  90. package/src/icons/lucide.ts +234 -0
  91. package/src/images/ImageError.ts +39 -0
  92. package/src/images/UploadTracker.ts +49 -0
  93. package/src/images/insertImages.ts +142 -0
  94. package/src/images/types.ts +22 -0
  95. package/src/images/upload.ts +62 -0
  96. package/src/index.ts +102 -0
  97. package/src/keymap/defaultKeymap.ts +189 -0
  98. package/src/keymap/keymap.ts +42 -0
  99. package/src/keymap/shortcut.ts +105 -0
  100. package/src/keymap/types.ts +40 -0
  101. package/src/math/FormulaView.tsx +62 -0
  102. package/src/math/mathCss.ts +4 -0
  103. package/src/math/renderer.ts +198 -0
  104. package/src/registry/defaultBlockComponents.ts +26 -0
  105. package/src/registry/resolveBlockComponent.ts +14 -0
  106. package/src/renderer/BlockRenderer.tsx +63 -0
  107. package/src/renderer/DocumentRenderer.tsx +33 -0
  108. package/src/renderer/DocumentView.tsx +94 -0
  109. package/src/renderer/virtual/VirtualChildList.tsx +355 -0
  110. package/src/renderer/virtual/estimate.ts +62 -0
  111. package/src/renderer/virtual/virtualRenderer.tsx +35 -0
  112. package/src/slash/DefaultSlashMenu.tsx +119 -0
  113. package/src/slash/SlashMenuController.ts +66 -0
  114. package/src/slash/SlashMenuHost.tsx +48 -0
  115. package/src/slash/applySlashItem.ts +57 -0
  116. package/src/slash/defaultSlashItems.tsx +186 -0
  117. package/src/slash/filter.ts +29 -0
  118. package/src/slash/items.ts +30 -0
  119. package/src/slash/syncSlashMenu.ts +67 -0
  120. package/src/slash/types.ts +21 -0
  121. package/src/toolbar/DefaultToolbar.tsx +281 -0
  122. package/src/toolbar/ToolbarController.ts +100 -0
  123. package/src/toolbar/ToolbarHost.tsx +81 -0
  124. package/src/toolbar/formatting.ts +75 -0
  125. package/src/toolbar/rich.ts +58 -0
  126. package/src/toolbar/syncToolbar.ts +39 -0
  127. package/src/toolbar/types.ts +21 -0
  128. package/src/ui/EditorUIContext.ts +26 -0
  129. package/src/ui/presentation.ts +24 -0
  130. package/src/ui/types.ts +70 -0
  131. package/README.md +0 -3
@@ -0,0 +1,328 @@
1
+ import type { LanguageDef } from '../languages';
2
+ import type { HighlightTokenType } from '../types';
3
+ import {
4
+ CHAR_LITERAL,
5
+ type Emit,
6
+ IDENT,
7
+ LIFETIME,
8
+ NUMBER,
9
+ REGEX_AFTER_WORD,
10
+ STRING_PREFIX,
11
+ isDigit,
12
+ isSpace,
13
+ lineEnd,
14
+ matchAt,
15
+ nextOnLine,
16
+ onlyWhitespaceBefore,
17
+ scanString,
18
+ } from './shared';
19
+
20
+ interface WordSets {
21
+ keywords: ReadonlySet<string>;
22
+ types: ReadonlySet<string>;
23
+ builtins: ReadonlySet<string>;
24
+ constants: ReadonlySet<string>;
25
+ }
26
+
27
+ const setsCache = new WeakMap<LanguageDef, WordSets>();
28
+
29
+ function wordSets(lang: LanguageDef): WordSets {
30
+ let sets = setsCache.get(lang);
31
+
32
+ if (!sets) {
33
+ const make = (list = '') =>
34
+ new Set(
35
+ list
36
+ .split(/\s+/)
37
+ .filter(Boolean)
38
+ .map((word) => (lang.ignoreCase ? word.toLowerCase() : word)),
39
+ );
40
+
41
+ sets = {
42
+ keywords: make(lang.keywords),
43
+ types: make(lang.types),
44
+ builtins: make(lang.builtins),
45
+ constants: make(lang.constants),
46
+ };
47
+ setsCache.set(lang, sets);
48
+ }
49
+
50
+ return sets;
51
+ }
52
+
53
+ function classify(
54
+ word: string,
55
+ lang: LanguageDef,
56
+ sets: WordSets,
57
+ followedByCall: boolean,
58
+ previousWord: string,
59
+ ): HighlightTokenType | null {
60
+ const key = lang.ignoreCase ? word.toLowerCase() : word;
61
+ const declared = lang.declarations?.[previousWord];
62
+
63
+ if (declared && !sets.keywords.has(key)) return declared;
64
+ if (sets.keywords.has(key)) return 'keyword';
65
+ if (sets.constants.has(key)) return 'constant';
66
+ if (sets.types.has(key)) return 'type';
67
+ if (sets.builtins.has(key)) return 'builtin';
68
+ if (followedByCall) return 'function';
69
+ if (lang.pascalTypes && /^[A-Z]/.test(word) && /[a-z]/.test(word)) return 'type';
70
+
71
+ return null;
72
+ }
73
+
74
+ export function scanGeneric(
75
+ code: string,
76
+ lang: LanguageDef,
77
+ start: number,
78
+ end: number,
79
+ emit: Emit,
80
+ ) {
81
+ const sets = wordSets(lang);
82
+ const quotes = lang.strings ?? '';
83
+ let i = start;
84
+ let previousWord = '';
85
+ /** Последний значимый токен: нужен, чтобы отличить регулярное выражение от деления. */
86
+ let last = '';
87
+
88
+ while (i < end) {
89
+ const char = code[i];
90
+
91
+ if (isSpace(char)) {
92
+ i++;
93
+ continue;
94
+ }
95
+
96
+ const lineMarker = lang.lineComment?.find(
97
+ (marker) =>
98
+ code.startsWith(marker, i) &&
99
+ (!lang.commentAtWordStart || i === start || isSpace(code[i - 1])),
100
+ );
101
+
102
+ if (lineMarker) {
103
+ const stop = lineEnd(code, i, end);
104
+
105
+ emit(i, stop, 'comment');
106
+ i = stop;
107
+ previousWord = '';
108
+ continue;
109
+ }
110
+
111
+ if (lang.blockComment && code.startsWith(lang.blockComment[0], i)) {
112
+ const close = code.indexOf(lang.blockComment[1], i + lang.blockComment[0].length);
113
+ const stop =
114
+ close < 0 || close + lang.blockComment[1].length > end
115
+ ? end
116
+ : close + lang.blockComment[1].length;
117
+
118
+ emit(i, stop, 'comment');
119
+ i = stop;
120
+ previousWord = '';
121
+ continue;
122
+ }
123
+
124
+ if (lang.preprocessor && char === '#' && onlyWhitespaceBefore(code, i, start)) {
125
+ const stop = lineEnd(code, i, end);
126
+
127
+ emit(i, stop, 'meta');
128
+ i = stop;
129
+ previousWord = '';
130
+ continue;
131
+ }
132
+
133
+ if (
134
+ lang.rustAttributes &&
135
+ char === '#' &&
136
+ (code[i + 1] === '[' || code.startsWith('![', i + 1))
137
+ ) {
138
+ let depth = 0;
139
+ let stop = i;
140
+
141
+ for (; stop < end; stop++) {
142
+ if (code[stop] === '[') depth++;
143
+ else if (code[stop] === ']' && --depth <= 0) {
144
+ stop++;
145
+ break;
146
+ } else if (code[stop] === '\n') break;
147
+ }
148
+
149
+ emit(i, stop, 'meta');
150
+ i = stop;
151
+ previousWord = '';
152
+ continue;
153
+ }
154
+
155
+ if (lang.lifetimes && char === "'") {
156
+ const literal = matchAt(CHAR_LITERAL, code, i);
157
+
158
+ if (literal) {
159
+ emit(i, i + literal.length, 'string');
160
+ i += literal.length;
161
+ } else {
162
+ const lifetime = matchAt(LIFETIME, code, i);
163
+
164
+ if (lifetime) emit(i, i + lifetime.length, 'type');
165
+ i += lifetime ? lifetime.length : 1;
166
+ }
167
+
168
+ previousWord = '';
169
+ last = 'value';
170
+ continue;
171
+ }
172
+
173
+ if (quotes.includes(char)) {
174
+ const triple = lang.triple === true && code.startsWith(char.repeat(3), i);
175
+ const stop = scanString(
176
+ code,
177
+ i,
178
+ end,
179
+ char,
180
+ triple,
181
+ triple || (lang.multiline ?? '').includes(char),
182
+ );
183
+ let type: HighlightTokenType = 'string';
184
+
185
+ if (lang.stringKeys && code[nextOnLine(code, stop, end)] === ':') type = 'property';
186
+
187
+ emit(i, stop, type);
188
+ i = stop;
189
+ previousWord = '';
190
+ last = 'value';
191
+ continue;
192
+ }
193
+
194
+ if (isDigit(char) || (char === '.' && isDigit(code[i + 1]))) {
195
+ const number = matchAt(NUMBER, code, i) ?? char;
196
+
197
+ emit(i, i + number.length, 'number');
198
+ i += number.length;
199
+ previousWord = '';
200
+ last = 'value';
201
+ continue;
202
+ }
203
+
204
+ if (lang.dollar && char === '$') {
205
+ let stop = i + 1;
206
+
207
+ if (code[stop] === '{') {
208
+ const close = code.indexOf('}', stop);
209
+
210
+ stop = close < 0 || close >= end ? lineEnd(code, stop, end) : close + 1;
211
+ } else if (/[\w@?#*!$-]/.test(code[stop] ?? '')) {
212
+ const word = matchAt(/\w+|[@?#*!$-]/y, code, stop);
213
+
214
+ stop += word ? word.length : 1;
215
+ }
216
+
217
+ if (stop > i + 1) emit(i, stop, 'variable');
218
+ i = Math.max(stop, i + 1);
219
+ previousWord = '';
220
+ last = 'value';
221
+ continue;
222
+ }
223
+
224
+ if (
225
+ lang.flags &&
226
+ char === '-' &&
227
+ (i === start || isSpace(code[i - 1])) &&
228
+ /[A-Za-z-]/.test(code[i + 1] ?? '')
229
+ ) {
230
+ let stop = i + 1;
231
+
232
+ while (stop < end && !isSpace(code[stop]) && code[stop] !== '=') stop++;
233
+
234
+ emit(i, stop, 'attribute');
235
+ i = stop;
236
+ previousWord = '';
237
+ continue;
238
+ }
239
+
240
+ if (lang.at && char === '@') {
241
+ const name = matchAt(/[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/y, code, i + 1);
242
+
243
+ if (name) {
244
+ emit(i, i + 1 + name.length, 'meta');
245
+ i += 1 + name.length;
246
+ previousWord = '';
247
+ continue;
248
+ }
249
+ }
250
+
251
+ const word = matchAt(IDENT, code, i);
252
+
253
+ if (word) {
254
+ const stop = i + word.length;
255
+
256
+ if (
257
+ lang.stringPrefixes &&
258
+ STRING_PREFIX.test(word) &&
259
+ stop < end &&
260
+ quotes.includes(code[stop])
261
+ ) {
262
+ const quote = code[stop];
263
+ const triple = lang.triple === true && code.startsWith(quote.repeat(3), stop);
264
+
265
+ const close = scanString(code, stop, end, quote, triple, triple);
266
+
267
+ emit(i, close, 'string');
268
+ i = close;
269
+ previousWord = '';
270
+ last = 'value';
271
+ continue;
272
+ }
273
+
274
+ if (lang.macros && code[stop] === '!' && code[stop + 1] !== '=') {
275
+ emit(i, stop + 1, 'function');
276
+ i = stop + 1;
277
+ previousWord = '';
278
+ last = 'value';
279
+ continue;
280
+ }
281
+
282
+ const followedByCall = code[nextOnLine(code, stop, end)] === '(';
283
+ const type = classify(word, lang, sets, followedByCall, previousWord);
284
+
285
+ if (type) emit(i, stop, type);
286
+
287
+ previousWord = word;
288
+ last = `word:${word}`;
289
+ i = stop;
290
+ continue;
291
+ }
292
+
293
+ if (lang.regex && char === '/') {
294
+ const afterWord = last.startsWith('word:') && REGEX_AFTER_WORD.has(last.slice(5));
295
+ const allowed = last === '' || last.length === 1 || afterWord;
296
+
297
+ if (allowed) {
298
+ let stop = i + 1;
299
+ let inClass = false;
300
+
301
+ while (stop < end && code[stop] !== '\n') {
302
+ if (code[stop] === '\\') stop++;
303
+ else if (code[stop] === '[') inClass = true;
304
+ else if (code[stop] === ']') inClass = false;
305
+ else if (code[stop] === '/' && !inClass) break;
306
+
307
+ stop++;
308
+ }
309
+
310
+ if (code[stop] === '/') {
311
+ stop++;
312
+
313
+ while (stop < end && /[a-z]/i.test(code[stop])) stop++;
314
+
315
+ emit(i, stop, 'regex');
316
+ i = stop;
317
+ previousWord = '';
318
+ last = 'value';
319
+ continue;
320
+ }
321
+ }
322
+ }
323
+
324
+ previousWord = '';
325
+ last = char === ')' || char === ']' || char === '}' ? 'value' : char;
326
+ i++;
327
+ }
328
+ }
@@ -0,0 +1,129 @@
1
+ import { LANGUAGES } from '../languages';
2
+ import { scanCss } from './css';
3
+ import { scanGeneric } from './generic';
4
+ import { type Emit, isSpace, matchAt } from './shared';
5
+
6
+ const ENTITY = /&(?:#\d+|#x[\da-fA-F]+|[A-Za-z]\w*);/g;
7
+
8
+ function emitEntities(code: string, from: number, to: number, emit: Emit) {
9
+ ENTITY.lastIndex = from;
10
+
11
+ for (let match = ENTITY.exec(code); match && match.index < to; match = ENTITY.exec(code)) {
12
+ if (match.index + match[0].length <= to)
13
+ emit(match.index, match.index + match[0].length, 'constant');
14
+ }
15
+ }
16
+
17
+ export function scanHtml(code: string, start: number, end: number, emit: Emit) {
18
+ let i = start;
19
+
20
+ while (i < end) {
21
+ const open = code.indexOf('<', i);
22
+
23
+ if (open < 0 || open >= end) {
24
+ emitEntities(code, i, end, emit);
25
+ return;
26
+ }
27
+
28
+ emitEntities(code, i, open, emit);
29
+
30
+ if (code.startsWith('<!--', open)) {
31
+ const close = code.indexOf('-->', open + 4);
32
+ const stop = close < 0 || close + 3 > end ? end : close + 3;
33
+
34
+ emit(open, stop, 'comment');
35
+ i = stop;
36
+ continue;
37
+ }
38
+
39
+ if (code.startsWith('<!', open) || code.startsWith('<?', open)) {
40
+ const close = code.indexOf('>', open);
41
+ const stop = close < 0 || close >= end ? end : close + 1;
42
+
43
+ emit(open, stop, 'meta');
44
+ i = stop;
45
+ continue;
46
+ }
47
+
48
+ const name = matchAt(/<\/?[A-Za-z][\w:.-]*/y, code, open);
49
+
50
+ if (!name) {
51
+ i = open + 1;
52
+ continue;
53
+ }
54
+
55
+ emit(open, open + name.length, 'tag');
56
+ i = open + name.length;
57
+
58
+ let selfClosing = false;
59
+
60
+ while (i < end) {
61
+ while (i < end && isSpace(code[i])) i++;
62
+
63
+ if (code[i] === '>') {
64
+ emit(i, i + 1, 'tag');
65
+ i++;
66
+ break;
67
+ }
68
+
69
+ if (code.startsWith('/>', i)) {
70
+ emit(i, i + 2, 'tag');
71
+ selfClosing = true;
72
+ i += 2;
73
+ break;
74
+ }
75
+
76
+ const attribute = matchAt(/[^\s=<>"'/]+/y, code, i);
77
+
78
+ if (!attribute) {
79
+ i++;
80
+ continue;
81
+ }
82
+
83
+ emit(i, i + attribute.length, 'attribute');
84
+ i += attribute.length;
85
+
86
+ let after = i;
87
+
88
+ while (after < end && isSpace(code[after])) after++;
89
+
90
+ if (code[after] !== '=') continue;
91
+
92
+ after++;
93
+
94
+ while (after < end && isSpace(code[after])) after++;
95
+
96
+ const quote = code[after];
97
+
98
+ if (quote === '"' || quote === "'") {
99
+ const close = code.indexOf(quote, after + 1);
100
+ const stop = close < 0 || close >= end ? end : close + 1;
101
+
102
+ emit(after, stop, 'string');
103
+ i = stop;
104
+ } else {
105
+ const value = matchAt(/[^\s>]+/y, code, after);
106
+
107
+ if (value) emit(after, after + value.length, 'string');
108
+ i = after + (value ? value.length : 0);
109
+ }
110
+ }
111
+
112
+ const tag = name.replace(/^<\/?/, '').toLowerCase();
113
+
114
+ if (
115
+ !selfClosing &&
116
+ !name.startsWith('</') &&
117
+ (tag === 'script' || tag === 'style') &&
118
+ i < end
119
+ ) {
120
+ const close = code.toLowerCase().indexOf(`</${tag}`, i);
121
+ const stop = close < 0 || close > end ? end : close;
122
+
123
+ if (tag === 'script') scanGeneric(code, LANGUAGES.javascript, i, stop, emit);
124
+ else scanCss(code, i, stop, emit);
125
+
126
+ i = stop;
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,58 @@
1
+ import { type Emit, lineEnd } from './shared';
2
+
3
+ const MARKDOWN_INLINE = /`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\[[^\]\n]*\]\([^)\n]*\)/g;
4
+
5
+ export function scanMarkdown(code: string, start: number, end: number, emit: Emit) {
6
+ let inFence = false;
7
+ let lineStart = start;
8
+
9
+ while (lineStart <= end) {
10
+ const stop = lineEnd(code, lineStart, end);
11
+ const line = code.slice(lineStart, stop);
12
+
13
+ if (/^\s*(```|~~~)/.test(line)) {
14
+ emit(lineStart, stop, 'meta');
15
+ inFence = !inFence;
16
+ } else if (inFence) {
17
+ emit(lineStart, stop, 'string');
18
+ } else if (/^#{1,6}\s/.test(line)) {
19
+ emit(lineStart, stop, 'keyword');
20
+ } else if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
21
+ emit(lineStart, stop, 'meta');
22
+ } else {
23
+ let offset = 0;
24
+ const marker = /^\s*(?:>|[-*+]\s|\d+[.)]\s)/.exec(line);
25
+
26
+ if (marker) {
27
+ const length = marker[0].trimEnd().length;
28
+
29
+ emit(
30
+ lineStart + marker[0].length - marker[0].trimStart().length,
31
+ lineStart + length,
32
+ 'keyword',
33
+ );
34
+ offset = marker[0].length;
35
+ }
36
+
37
+ MARKDOWN_INLINE.lastIndex = offset;
38
+
39
+ for (let match = MARKDOWN_INLINE.exec(line); match; match = MARKDOWN_INLINE.exec(line)) {
40
+ const from = lineStart + match.index;
41
+ const text = match[0];
42
+
43
+ if (text.startsWith('[')) {
44
+ const split = text.indexOf('](');
45
+
46
+ emit(from, from + split + 1, 'function');
47
+ emit(from + split + 1, from + text.length, 'string');
48
+ } else {
49
+ emit(from, from + text.length, text.startsWith('`') ? 'string' : 'keyword');
50
+ }
51
+ }
52
+ }
53
+
54
+ if (stop >= end) break;
55
+
56
+ lineStart = stop + 1;
57
+ }
58
+ }
@@ -0,0 +1,81 @@
1
+ import type { HighlightTokenType } from '../types';
2
+
3
+ export type Emit = (from: number, to: number, type: HighlightTokenType) => void;
4
+
5
+ export const IDENT = /[A-Za-z_$À-￿][\w$À-￿]*/y;
6
+ export const NUMBER =
7
+ /(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*(?:\.[\d_]+)?|\.\d[\d_]*)(?:[eE][+-]?\d+)?)[a-zA-Z]*/y;
8
+ export const CHAR_LITERAL = /'(?:\\.|[^'\\\n])'/y;
9
+ export const LIFETIME = /'[A-Za-z_]\w*/y;
10
+ export const STRING_PREFIX = /^[rRbBfFuU]{1,3}$/;
11
+ export const REGEX_AFTER_WORD = new Set(
12
+ 'return typeof case delete void throw in of new else do yield await'.split(' '),
13
+ );
14
+
15
+ export const matchAt = (re: RegExp, code: string, at: number): string | null => {
16
+ re.lastIndex = at;
17
+
18
+ const match = re.exec(code);
19
+
20
+ return match ? match[0] : null;
21
+ };
22
+
23
+ export const isDigit = (char: string | undefined) =>
24
+ char !== undefined && char >= '0' && char <= '9';
25
+ export const isSpace = (char: string | undefined) =>
26
+ char === ' ' || char === '\t' || char === '\n' || char === '\r';
27
+
28
+ export function lineEnd(code: string, from: number, end: number): number {
29
+ const index = code.indexOf('\n', from);
30
+
31
+ return index < 0 || index > end ? end : index;
32
+ }
33
+
34
+ export function nextOnLine(code: string, from: number, end: number): number {
35
+ let i = from;
36
+
37
+ while (i < end && (code[i] === ' ' || code[i] === '\t')) i++;
38
+
39
+ return i;
40
+ }
41
+
42
+ export function onlyWhitespaceBefore(code: string, at: number, start: number): boolean {
43
+ for (let i = at - 1; i >= start && code[i] !== '\n'; i--) {
44
+ if (code[i] !== ' ' && code[i] !== '\t') return false;
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ export function scanString(
51
+ code: string,
52
+ start: number,
53
+ end: number,
54
+ quote: string,
55
+ triple: boolean,
56
+ multiline: boolean,
57
+ ): number {
58
+ const closing = quote.repeat(3);
59
+ let i = start + (triple ? 3 : 1);
60
+
61
+ while (i < end) {
62
+ const char = code[i];
63
+
64
+ if (char === '\\') {
65
+ i += 2;
66
+ continue;
67
+ }
68
+
69
+ if (triple) {
70
+ if (code.startsWith(closing, i)) return i + 3;
71
+ } else if (char === quote) {
72
+ return i + 1;
73
+ } else if (char === '\n' && !multiline) {
74
+ return i;
75
+ }
76
+
77
+ i++;
78
+ }
79
+
80
+ return end;
81
+ }
@@ -0,0 +1,34 @@
1
+ import { LANGUAGES } from '../languages';
2
+ import { scanGeneric } from './generic';
3
+ import { type Emit, lineEnd } from './shared';
4
+
5
+ const YAML_KEY = /^(\s*(?:-\s+)*)("[^"]*"|'[^']*'|[^\s#:'"[\]{},&*!|>-][^#:]*?)\s*:(?=\s|$)/;
6
+
7
+ export function scanYaml(code: string, start: number, end: number, emit: Emit) {
8
+ let lineStart = start;
9
+
10
+ while (lineStart <= end) {
11
+ const stop = lineEnd(code, lineStart, end);
12
+ const line = code.slice(lineStart, stop);
13
+
14
+ if (/^(---|\.\.\.)\s*$/.test(line)) {
15
+ emit(lineStart, stop, 'meta');
16
+ } else {
17
+ const key = YAML_KEY.exec(line);
18
+ let restStart = lineStart;
19
+
20
+ if (key) {
21
+ const keyFrom = lineStart + key[1].length;
22
+
23
+ emit(keyFrom, keyFrom + key[2].length, 'property');
24
+ restStart = lineStart + key[0].length;
25
+ }
26
+
27
+ scanGeneric(code, LANGUAGES.yaml, restStart, stop, emit);
28
+ }
29
+
30
+ if (stop >= end) break;
31
+
32
+ lineStart = stop + 1;
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Тип токена подсветки. Стандартные типы получают цвета из `@inkbrick/styles` (`--inkbrick-syntax-*`); свой подсветчик может
3
+ * вернуть и любой другой тип — он попадёт в `data-inkbrick-token` как есть, и вы раскрасите его своим CSS.
4
+ */
5
+ export type HighlightTokenType =
6
+ | 'keyword'
7
+ | 'string'
8
+ | 'comment'
9
+ | 'number'
10
+ | 'function'
11
+ | 'type'
12
+ | 'constant'
13
+ | 'builtin'
14
+ | 'variable'
15
+ | 'property'
16
+ | 'tag'
17
+ | 'attribute'
18
+ | 'meta'
19
+ | 'regex'
20
+ | (string & {});
21
+
22
+ export interface HighlightToken {
23
+ readonly from: number;
24
+ readonly to: number;
25
+ readonly type: HighlightTokenType;
26
+ }
27
+
28
+ /**
29
+ * Подсветчик кода: по тексту и языку (`props.language` блока) возвращает токены. Результат не обязан быть
30
+ * упорядоченным и не должен быть идеальным: пересечения, выход за границы и неверные типы отбрасываются, а текст
31
+ * блока подсветка изменить не может. Встроенный подсветчик — `highlightCode`; подключить Shiki, Prism или
32
+ * highlight.js можно, обернув их в такую функцию (`<EditorProvider highlighter={…}>`).
33
+ */
34
+ export type CodeHighlighter = (code: string, language: string) => readonly HighlightToken[];
@@ -0,0 +1,9 @@
1
+ import { createContext } from 'react';
2
+
3
+ import type { Editor } from '@inkbrick/engine';
4
+
5
+ import type { EditorView } from '../editing/EditorView';
6
+
7
+ export const EditorContext = createContext<Editor | null>(null);
8
+
9
+ export const EditorViewContext = createContext<EditorView | null>(null);