minecodex 2.0.12 → 2.0.14

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 (69) hide show
  1. package/README.md +20 -8
  2. package/apps/dashboard/app.js +4 -4
  3. package/apps/dashboard/assets/data-transfer.js +129 -0
  4. package/apps/dashboard/assets/download.svg +15 -0
  5. package/apps/dashboard/assets/upload.svg +15 -0
  6. package/apps/dashboard/index.html +10 -0
  7. package/apps/dashboard/styles.css +50 -3
  8. package/features/file-navigation/assets/book-open.svg +16 -0
  9. package/features/file-navigation/assets/folder-tree.svg +16 -0
  10. package/features/file-navigation/codex-feature.json +27 -0
  11. package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
  12. package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
  13. package/features/file-navigation/src/domain/navigation.mjs +12 -0
  14. package/features/file-navigation/src/entry.mjs +34 -0
  15. package/features/file-navigation/src/frontend/navigation.mjs +140 -0
  16. package/features/file-navigation/src/injection-source.mjs +9 -0
  17. package/features/file-navigation/src/navigation.css +16 -0
  18. package/features/file-navigation/vendor/lucide-LICENSE +43 -0
  19. package/features/hide-upsell-banner/codex-feature.json +4 -4
  20. package/features/images/codex-feature.json +4 -4
  21. package/features/images/src/adapters/codex/native-image-modules.mjs +13 -2
  22. package/features/images/src/library-prompt-source.mjs +2 -2
  23. package/features/markdown-reading/assets/book-open.svg +16 -0
  24. package/features/markdown-reading/codex-feature.json +27 -0
  25. package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
  26. package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
  27. package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
  28. package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
  29. package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
  30. package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
  31. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
  32. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
  33. package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
  34. package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
  35. package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
  36. package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
  37. package/features/markdown-reading/src/injection-source.mjs +43 -0
  38. package/features/markdown-reading/src/markdown-reading.mjs +82 -0
  39. package/features/markdown-reading/src/reading.css +346 -0
  40. package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
  41. package/features/model-slider/codex-feature.json +5 -4
  42. package/features/model-slider/src/backend/favorites.mjs +54 -0
  43. package/features/model-slider/src/domain/model-rules.mjs +6 -1
  44. package/features/model-slider/src/frontend/install.mjs +24 -3
  45. package/features/notes/assets/prompt-previews.json +26 -0
  46. package/features/notes/codex-feature.json +6 -5
  47. package/features/notes/src/backend/transfer.mjs +97 -0
  48. package/features/notes/src/frontend/entry.mjs +380 -10
  49. package/features/notes/src/frontend/icons.json +4 -1
  50. package/features/notes/src/frontend/styles.css +63 -0
  51. package/features/session-status/codex-feature.json +6 -3
  52. package/features/session-status/src/domain/session-stats.mjs +22 -8
  53. package/features/session-status/src/session-status.mjs +2 -2
  54. package/features/sidebar-focus/codex-feature.json +2 -2
  55. package/features/sidebar-focus/src/sidebar-focus.mjs +13 -5
  56. package/features/sites/codex-feature.json +4 -4
  57. package/features/sites/src/backend/sites-service.mjs +20 -5
  58. package/features/sites/src/backend/transfer.mjs +43 -0
  59. package/features/usage-meter/codex-feature.json +6 -3
  60. package/package.json +11 -3
  61. package/packages/cli/src/config.mjs +4 -2
  62. package/packages/cli/src/control-server.mjs +34 -10
  63. package/packages/cli/src/data-transfer/service.mjs +42 -0
  64. package/packages/cli/src/runtime-manager.mjs +3 -0
  65. package/packages/runtime-host/src/codex-injection.mjs +2 -1
  66. package/packages/runtime-host/src/codex-runtime.mjs +24 -1
  67. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  68. package/packages/ui/src/native/theme.mjs +23 -1
  69. package/packages/ui/src/native/tokens.mjs +4 -4
@@ -0,0 +1,561 @@
1
+ import { collectMarkdownFootnotes } from '../../domain/footnotes.mjs';
2
+ import { captureMarkdownProviders } from './providers.mjs';
3
+
4
+ // 复用消息流的代码组件与 Markdown lexer;文件内容及编辑器仍由 Codex 持有。
5
+ export async function loadNativeMarkdownRichText(native) {
6
+ const links = [...document.querySelectorAll('link[rel="modulepreload"]')].map((n) => n.href);
7
+ const { sharedUrl } = native;
8
+ const sharedSource = await (await fetch(sharedUrl)).text(),
9
+ shared = await import(sharedUrl);
10
+ const codeAt = sharedSource.indexOf('id:`codeSnippet.wrap.disable`');
11
+ const codeSection = sharedSource.slice(
12
+ sharedSource.lastIndexOf('function ', codeAt),
13
+ sharedSource.indexOf('function ', codeAt),
14
+ );
15
+ const codeName = codeSection.match(/^function ([\w$]+)/)?.[1];
16
+ const sharedExports = sharedSource.slice(sharedSource.lastIndexOf('export{'));
17
+ const sharedBinding = (name) =>
18
+ shared[sharedExports.match(new RegExp(`${name.replace(/\$/g, '\\$')} as ([\\w$]+)`))?.[1]];
19
+ const cacheName = codeSection.match(/let t=\(0,([\w$]+)\.c\)/)?.[1];
20
+ const assignment = sharedSource.indexOf(`${cacheName}=`); // 从组件依赖找到其 lazy initializer。
21
+ const codeInit = sharedSource
22
+ .slice(sharedSource.lastIndexOf('function ', assignment))
23
+ .match(/^function ([\w$]+)/)?.[1];
24
+ if (!codeInit || !sharedBinding(codeInit) || !sharedBinding(codeName))
25
+ throw new Error('native-code-snippet-unavailable');
26
+ sharedBinding(codeInit)();
27
+ let mathUrl = links.find((u) => /\/katex-[^/]+\.js$/.test(u));
28
+ if (!mathUrl) {
29
+ const editorUrl = links.find((u) => /\/text-file-editor-extensions-[^/]+\.js$/.test(u));
30
+ if (editorUrl) {
31
+ const editorSource = await (await fetch(editorUrl)).text(),
32
+ specifier = editorSource.match(/import\(`(\.\/katex-[^`]+\.js)`\)/)?.[1];
33
+ if (specifier) mathUrl = new URL(specifier, editorUrl).href;
34
+ }
35
+ }
36
+ const katex = mathUrl ? (await import(mathUrl)).default : null;
37
+ if (!katex?.renderToString) throw new Error('native-katex-unavailable');
38
+ return { ...native, CodeSnippet: sharedBinding(codeName), katex };
39
+ }
40
+
41
+ export function installMarkdownRichText(view, api, native, codex) {
42
+ const { Decoration, StateField, StateEffect, Compartment, WidgetType, Prec } = api;
43
+ const {
44
+ React,
45
+ ReactDOM,
46
+ Markdown,
47
+ lexer,
48
+ CodeSnippet,
49
+ Actions,
50
+ Button,
51
+ CopyButton,
52
+ Tooltip,
53
+ wrapIcons,
54
+ katex,
55
+ } = native;
56
+ const h = React.createElement,
57
+ roots = new Set(),
58
+ footnoteReturns = new Map(),
59
+ codeStates = new Map();
60
+ const withProviders = captureMarkdownProviders(view.dom, codex);
61
+ const mount = (element, tree) => {
62
+ const root = ReactDOM.createRoot(element);
63
+ roots.add(root);
64
+ root.render(withProviders(React, tree));
65
+ return root;
66
+ };
67
+ function Code({ content, language, id }) {
68
+ const [wrap, setWrap] = React.useState(
69
+ () => (codeStates.get(id)?.source === content && codeStates.get(id).wrap) || false,
70
+ ),
71
+ [overflow, setOverflow] = React.useState(false),
72
+ ref = React.useRef(null);
73
+ React.useLayoutEffect(() => {
74
+ const element = ref.current?.querySelector('code')?.parentElement;
75
+ if (!element) return;
76
+ const check = () => {
77
+ setOverflow(element.scrollWidth > element.clientWidth + 1);
78
+ view.requestMeasure();
79
+ };
80
+ const observer = new ResizeObserver(check);
81
+ observer.observe(element);
82
+ const code = element.querySelector('code');
83
+ if (code) observer.observe(code);
84
+ check();
85
+ return () => observer.disconnect();
86
+ }, [wrap, content]);
87
+ const label = wrap ? '关闭代码换行' : '开启代码换行';
88
+ return h(
89
+ 'div',
90
+ { ref, className: 'mcx-reading-code-block group/app-widget' },
91
+ h(CodeSnippet, {
92
+ content,
93
+ language,
94
+ renderSvgAsImage: false,
95
+ showCopyButton: false,
96
+ showStickyRightContent: false,
97
+ shouldWrapCode: wrap,
98
+ stickyHeader: false,
99
+ codeContainerClassName: 'mcx-reading-code-scroll',
100
+ }),
101
+ h(
102
+ Actions,
103
+ { 'data-markdown-copy': 'exclude', className: 'bg-transparent' },
104
+ wrap || overflow
105
+ ? h(
106
+ Tooltip,
107
+ { tooltipContent: label, delayOpen: true },
108
+ h(
109
+ Button,
110
+ {
111
+ 'aria-label': label,
112
+ color: 'blockAction',
113
+ radius: 'full',
114
+ size: 'inputIcon',
115
+ onClick: () => {
116
+ codeStates.set(id, { source: content, wrap: !wrap });
117
+ setWrap(!wrap);
118
+ },
119
+ },
120
+ h(wrap ? wrapIcons.disable : wrapIcons.enable),
121
+ ),
122
+ )
123
+ : null,
124
+ h(CopyButton, {
125
+ ariaLabel: '复制代码',
126
+ buttonText: '复制代码',
127
+ iconOnly: true,
128
+ variant: 'blockAction',
129
+ onCopy: () => navigator.clipboard.writeText(content).then(() => true),
130
+ }),
131
+ ),
132
+ );
133
+ }
134
+ class Rich extends WidgetType {
135
+ constructor(kind, source, options = {}) {
136
+ super();
137
+ Object.assign(this, { kind, source, options });
138
+ }
139
+ eq(other) {
140
+ return (
141
+ this.kind === other.kind &&
142
+ this.source === other.source &&
143
+ JSON.stringify(this.options) === JSON.stringify(other.options)
144
+ );
145
+ }
146
+ get estimatedHeight() {
147
+ return this.kind === 'code' ? 92 + this.source.split('\n').length * 20 : -1;
148
+ }
149
+ toDOM() {
150
+ const element = document.createElement(
151
+ this.kind === 'code' || this.kind === 'html-block' ? 'div' : 'span',
152
+ );
153
+ element.className = `mcx-reading-rich mcx-reading-rich-${this.kind}`;
154
+ if (this.options.quoteDepth) {
155
+ element.style.paddingInlineStart = `${this.options.quoteDepth * 20}px`;
156
+ element.classList.add('mcx-reading-rich-quoted');
157
+ }
158
+ if (this.kind === 'text') element.textContent = this.source;
159
+ else if (this.kind === 'code')
160
+ element._mcxRoot = mount(
161
+ element,
162
+ h(Code, { content: this.source, language: this.options.language, id: this.options.from }),
163
+ );
164
+ else if (this.kind === 'math')
165
+ element.innerHTML = katex.renderToString(this.source, {
166
+ displayMode: false,
167
+ strict: 'ignore',
168
+ throwOnError: false,
169
+ });
170
+ else if (this.kind === 'footnote-ref') {
171
+ const sup = document.createElement('sup'),
172
+ button = document.createElement('button');
173
+ button.textContent = this.options.number;
174
+ button.type = 'button';
175
+ button.setAttribute('aria-label', `脚注 ${this.options.number}`);
176
+ button.dataset.mcxFootnoteRef = this.options.id;
177
+ button.addEventListener('click', (event) => {
178
+ event.preventDefault();
179
+ footnoteReturns.set(this.options.id, view.scrollDOM.scrollTop);
180
+ jump(this.options.to);
181
+ });
182
+ sup.append(button);
183
+ element.append(sup);
184
+ } else if (this.kind === 'footnote-body') {
185
+ element.classList.add('mcx-reading-footnote-body');
186
+ const number = document.createElement('span');
187
+ number.textContent = `${this.options.number}. `;
188
+ element.append(number);
189
+ const body = document.createElement('span');
190
+ body.className = 'mcx-reading-footnote-content';
191
+ element.append(body);
192
+ element._mcxRoot = mount(
193
+ body,
194
+ h(Markdown, { tokens: lexer(this.source, undefined), components: { p: React.Fragment } }),
195
+ );
196
+ const back = document.createElement('button');
197
+ back.type = 'button';
198
+ back.textContent = '↩';
199
+ back.setAttribute('aria-label', '返回脚注引用');
200
+ back.addEventListener('click', () => {
201
+ const previous = footnoteReturns.get(this.options.id);
202
+ if (previous === undefined) jump(this.options.to);
203
+ else {
204
+ view.scrollDOM.scrollTop = previous;
205
+ view.requestMeasure();
206
+ }
207
+ });
208
+ element.append(back);
209
+ } else if (this.kind === 'html' || this.kind === 'html-block') {
210
+ const parsed = new DOMParser().parseFromString(this.source, 'text/html');
211
+ const allowed = new Set([
212
+ 'P',
213
+ 'DIV',
214
+ 'SPAN',
215
+ 'BR',
216
+ 'MARK',
217
+ 'KBD',
218
+ 'STRONG',
219
+ 'B',
220
+ 'EM',
221
+ 'I',
222
+ 'S',
223
+ 'DEL',
224
+ 'U',
225
+ 'SUB',
226
+ 'SUP',
227
+ 'SMALL',
228
+ 'BLOCKQUOTE',
229
+ 'UL',
230
+ 'OL',
231
+ 'LI',
232
+ 'DL',
233
+ 'DT',
234
+ 'DD',
235
+ 'PRE',
236
+ 'CODE',
237
+ 'H1',
238
+ 'H2',
239
+ 'H3',
240
+ 'H4',
241
+ 'H5',
242
+ 'H6',
243
+ 'HR',
244
+ 'TABLE',
245
+ 'THEAD',
246
+ 'TBODY',
247
+ 'TR',
248
+ 'TD',
249
+ 'TH',
250
+ 'A',
251
+ 'DETAILS',
252
+ 'SUMMARY',
253
+ ]);
254
+ const clean = (node) => {
255
+ if (node.nodeType === Node.TEXT_NODE) return document.createTextNode(node.textContent);
256
+ if (
257
+ node.nodeType !== Node.ELEMENT_NODE ||
258
+ ['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH'].includes(node.tagName)
259
+ )
260
+ return document.createDocumentFragment();
261
+ const el = allowed.has(node.tagName)
262
+ ? document.createElement(node.tagName.toLowerCase())
263
+ : document.createDocumentFragment();
264
+ if (el.nodeType === Node.ELEMENT_NODE) {
265
+ for (const name of ['title', 'colspan', 'rowspan', 'start', 'open'])
266
+ if (node.hasAttribute(name)) el.setAttribute(name, node.getAttribute(name));
267
+ if (node.tagName === 'A') {
268
+ const href = node.getAttribute('href') || '';
269
+ if (/^(?:javascript|data|vbscript):/i.test(href))
270
+ return document.createTextNode(node.textContent);
271
+ const link = document.createElement('span');
272
+ const root = mount(
273
+ link,
274
+ h(Markdown, {
275
+ tokens: lexer(`[${node.textContent.replace(/([\\\[\]])/g, '\\$1')}](${href})`, undefined),
276
+ components: { p: React.Fragment },
277
+ }),
278
+ );
279
+ element._mcxChildRoots ??= [];
280
+ element._mcxChildRoots.push(root);
281
+ return link;
282
+ }
283
+ }
284
+ for (const child of node.childNodes) el.append(clean(child));
285
+ return el;
286
+ };
287
+ for (const child of parsed.body.childNodes) element.append(clean(child));
288
+ }
289
+ requestAnimationFrame(() => {
290
+ if (!view.destroyed) view.requestMeasure();
291
+ });
292
+ return element;
293
+ }
294
+ destroy(element) {
295
+ for (const root of element._mcxChildRoots || []) {
296
+ roots.delete(root);
297
+ root.unmount();
298
+ }
299
+ if (element._mcxRoot) {
300
+ roots.delete(element._mcxRoot);
301
+ element._mcxRoot.unmount();
302
+ }
303
+ }
304
+ ignoreEvent() {
305
+ return true;
306
+ }
307
+ }
308
+ function jump(pos) {
309
+ view.dispatch({ effects: view.constructor.scrollIntoView(pos, { y: 'start', yMargin: 24 }) });
310
+ view.requestMeasure();
311
+ }
312
+ function build(state) {
313
+ const ranges = [],
314
+ doc = state.doc,
315
+ text = doc.toString(),
316
+ tree = state.tree;
317
+ if (!tree) return Decoration.none;
318
+ const selected = (from, to) =>
319
+ view.hasFocus && state.selection.ranges.some((r) => !r.empty && r.from < to && r.to > from);
320
+ const hiddenLines = new Set(),
321
+ quotes = new Map(),
322
+ alerts = new Map(),
323
+ protectedRanges = [];
324
+ tree.iterate({
325
+ enter(n) {
326
+ if (
327
+ [
328
+ 'FencedCode',
329
+ 'CodeBlock',
330
+ 'InlineCode',
331
+ 'HTMLBlock',
332
+ 'HTMLTag',
333
+ 'CommentBlock',
334
+ 'Comment',
335
+ ].includes(n.name)
336
+ )
337
+ protectedRanges.push([n.from, n.to]);
338
+ },
339
+ });
340
+ const protectedAt = (pos) => protectedRanges.some(([from, to]) => pos >= from && pos < to);
341
+ const replace = (from, to, widget, block = false) => {
342
+ if (to > from && !selected(from, to))
343
+ ranges.push(Decoration.replace({ widget, block }).range(from, to));
344
+ };
345
+ tree.iterate({
346
+ enter(n) {
347
+ const node = n.node,
348
+ raw = state.sliceDoc(n.from, n.to),
349
+ line = doc.lineAt(n.from);
350
+ if (
351
+ n.name === 'MathInline' &&
352
+ raw.startsWith('\\[') &&
353
+ doc.lineAt(n.from).number === doc.lineAt(n.to).number
354
+ ) {
355
+ replace(n.from, n.to, new Rich('text', `[${raw.slice(2, -2)}]`));
356
+ return false;
357
+ }
358
+ if (n.name === 'Escape') {
359
+ replace(n.from, n.from + 1);
360
+ return false;
361
+ }
362
+ if (n.name === 'HardBreak') {
363
+ if (raw.startsWith('\\')) replace(n.from, n.from + 1);
364
+ return false;
365
+ }
366
+ if (n.name === 'HeaderMark' && node.parent?.name.startsWith('SetextHeading')) {
367
+ hiddenLines.add(line.from);
368
+ return false;
369
+ }
370
+ if (n.name === 'Blockquote') {
371
+ let depth = 0;
372
+ for (let parent = node; parent; parent = parent.parent) if (parent.name === 'Blockquote') depth++;
373
+ const last = doc.lineAt(n.to),
374
+ callout = raw.split('\n', 1)[0].match(/^\s*>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/);
375
+ for (let i = line.number; i <= last.number; i++) {
376
+ const l = doc.line(i);
377
+ quotes.set(l.from, Math.max(quotes.get(l.from) || 0, depth));
378
+ if (callout)
379
+ alerts.set(
380
+ l.from,
381
+ `${i === line.number ? ' mcx-reading-callout-first' : ''}${i === last.number ? ' mcx-reading-callout-last' : ''}`,
382
+ );
383
+ }
384
+ }
385
+ if (n.name === 'FencedCode' || n.name === 'CodeBlock') {
386
+ const language = node.getChild('CodeInfo')
387
+ ? state.sliceDoc(node.getChild('CodeInfo').from, node.getChild('CodeInfo').to).split(/\s/)[0]
388
+ : undefined;
389
+ if (['mermaid', 'math', 'latex', 'tex'].includes(language)) return false;
390
+ const first = doc.lineAt(n.from),
391
+ last = doc.lineAt(n.to),
392
+ fenced = n.name === 'FencedCode',
393
+ marks = node.getChildren('CodeMark');
394
+ const from = first.from,
395
+ to = last.to,
396
+ closed = fenced && marks.length === 2;
397
+ let quoteDepth = 0;
398
+ for (let parent = node.parent; parent; parent = parent.parent)
399
+ if (parent.name === 'Blockquote') quoteDepth++;
400
+ const unquote = (line) => {
401
+ for (let i = 0; i < quoteDepth; i++) line = line.replace(/^[ \t]*>[ \t]?/, '');
402
+ return line;
403
+ };
404
+ const prefixWidth = first.text.length - unquote(first.text).length;
405
+ const sourceIndent = Math.max(
406
+ 0,
407
+ (fenced ? (marks[0]?.from ?? n.from) : n.from) - first.from - prefixWidth,
408
+ );
409
+ const lines = [];
410
+ for (let i = first.number + (fenced ? 1 : 0); i <= last.number - (closed ? 1 : 0); i++) {
411
+ const line = unquote(doc.line(i).text);
412
+ lines.push(
413
+ line.startsWith('\t') && !fenced
414
+ ? line.slice(1)
415
+ : line.replace(new RegExp(`^ {0,${sourceIndent}}`), ''),
416
+ );
417
+ }
418
+ replace(from, to, new Rich('code', lines.join('\n'), { language, quoteDepth, from }), true);
419
+ return false;
420
+ }
421
+ if (n.name === 'Comment' || n.name === 'CommentBlock') {
422
+ replace(n.from, n.to);
423
+ if (line.from === n.from && doc.lineAt(n.to).to === n.to) hiddenLines.add(line.from);
424
+ return false;
425
+ }
426
+ if (n.name === 'HTMLBlock') {
427
+ replace(line.from, doc.lineAt(n.to).to, new Rich('html-block', raw), true);
428
+ return false;
429
+ }
430
+ if (n.name === 'HTMLTag') {
431
+ const opening = raw.match(/^<([a-z][\w-]*)\b[^>]*>$/i);
432
+ if (opening && !/\/\s*>$/.test(raw) && !['br', 'hr'].includes(opening[1].toLowerCase())) {
433
+ const rest = text.slice(n.to, doc.lineAt(n.to).to),
434
+ closing = new RegExp(`</${opening[1]}\\s*>`, 'i').exec(rest);
435
+ if (closing) {
436
+ const to = n.to + closing.index + closing[0].length;
437
+ replace(n.from, to, new Rich('html', text.slice(n.from, to)));
438
+ return false;
439
+ }
440
+ }
441
+ if (/^<(br|hr)\s*\/?\s*>$/i.test(raw)) replace(n.from, n.to, new Rich('html', raw));
442
+ return false;
443
+ }
444
+ },
445
+ });
446
+ for (const [from, depth] of quotes) {
447
+ const line = doc.lineAt(from),
448
+ prefix = line.text.match(/^(?:[ \t]*>[ \t]?)+/);
449
+ ranges.push(
450
+ Decoration.line({
451
+ class: `mcx-reading-quote${alerts.has(from) ? ' mcx-reading-callout' + alerts.get(from) : ''}`,
452
+ attributes: { style: `--mcx-quote-indent:${depth * 20}px` },
453
+ }).range(from),
454
+ );
455
+ if (prefix && !/^(?:[-+*]|\d+[.)])\s/.test(line.text.slice(prefix[0].length)))
456
+ replace(from, from + prefix[0].length);
457
+ const alert = line.text
458
+ .slice(prefix?.[0].length || 0)
459
+ .match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/);
460
+ if (alert) {
461
+ ranges.push(Decoration.line({ class: 'mcx-reading-alert-title' }).range(from));
462
+ replace(
463
+ from + (prefix?.[0].length || 0),
464
+ line.to,
465
+ new (class extends WidgetType {
466
+ toDOM() {
467
+ const span = document.createElement('span');
468
+ span.textContent = {
469
+ NOTE: '提示',
470
+ TIP: '建议',
471
+ IMPORTANT: '重要',
472
+ WARNING: '警告',
473
+ CAUTION: '注意',
474
+ }[alert[1]];
475
+ return span;
476
+ }
477
+ })(),
478
+ );
479
+ }
480
+ }
481
+ const { definitions: footnotes, references } = collectMarkdownFootnotes(text, protectedRanges);
482
+ for (const reference of references)
483
+ replace(
484
+ reference.from,
485
+ reference.to,
486
+ new Rich('footnote-ref', '', {
487
+ id: reference.id,
488
+ number: reference.number,
489
+ to: reference.definitionFrom,
490
+ }),
491
+ );
492
+ for (const [id, note] of footnotes)
493
+ replace(
494
+ note.from,
495
+ note.to,
496
+ new Rich('footnote-body', note.body, {
497
+ id,
498
+ number: note.number,
499
+ to: note.refs[0] ?? note.from,
500
+ }),
501
+ note.from !== doc.lineAt(note.to).from,
502
+ );
503
+ // 单美元公式与定义列表只处理普通正文,跳过代码、HTML 和脚注定义。
504
+ for (let i = 1; i <= doc.lines; i++) {
505
+ const line = doc.line(i);
506
+ if (
507
+ protectedAt(line.from) ||
508
+ [...footnotes.values()].some((n) => line.from >= n.from && line.from <= n.to)
509
+ )
510
+ continue;
511
+ for (const m of line.text.matchAll(/(?<![\\$])\$(?![\s$])([^$\n]+?)(?<![\s\\])\$(?!\$)/g)) {
512
+ const from = line.from + m.index;
513
+ if (!protectedAt(from)) replace(from, from + m[0].length, new Rich('math', m[1]));
514
+ }
515
+ if (/^: +\S/.test(line.text) && i > 1) {
516
+ const term = doc.line(i - 1);
517
+ if (term.text.trim() && !/^: +/.test(term.text))
518
+ ranges.push(Decoration.line({ class: 'mcx-reading-definition-term' }).range(term.from));
519
+ ranges.push(Decoration.line({ class: 'mcx-reading-definition-body' }).range(line.from));
520
+ replace(line.from, line.from + line.text.match(/^: +/)[0].length);
521
+ }
522
+ }
523
+ for (const from of hiddenLines)
524
+ if (!selected(from, doc.lineAt(from).to))
525
+ ranges.push(Decoration.line({ class: 'mcx-reading-syntax-line' }).range(from));
526
+ // HTML 成对标签和块替换覆盖内部装饰;只保留最外层 replacement。
527
+ const replacements = ranges
528
+ .filter((r) => r.value.isReplace)
529
+ .sort((a, b) => a.from - b.from || b.to - a.to),
530
+ accepted = [];
531
+ for (const r of replacements) if (!accepted.some((a) => r.from < a.to && r.to > a.from)) accepted.push(r);
532
+ return Decoration.set(
533
+ [
534
+ ...ranges.filter(
535
+ (r) =>
536
+ !r.value.isReplace && !accepted.some((a) => a.value.block && r.from >= a.from && r.from < a.to),
537
+ ),
538
+ ...accepted,
539
+ ],
540
+ true,
541
+ );
542
+ }
543
+ const field = StateField.define({
544
+ create: build,
545
+ update(value, transaction) {
546
+ return transaction.docChanged ||
547
+ transaction.selection ||
548
+ transaction.startState.tree !== transaction.state.tree
549
+ ? build(transaction.state)
550
+ : value;
551
+ },
552
+ provide: (field) => view.constructor.decorations.from(field),
553
+ });
554
+ const compartment = new Compartment();
555
+ view.dispatch({ effects: StateEffect.appendConfig.of(compartment.of(Prec.highest(field))) });
556
+ return () => {
557
+ if (!view.destroyed) view.dispatch({ effects: compartment.reconfigure([]) });
558
+ for (const root of roots) root.unmount();
559
+ roots.clear();
560
+ };
561
+ }