minecodex 2.0.12 → 2.0.13

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 (59) hide show
  1. package/README.md +12 -0
  2. package/apps/dashboard/app.js +37 -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 +52 -1
  8. package/features/file-navigation/assets/book-open.svg +16 -0
  9. package/features/file-navigation/codex-feature.json +27 -0
  10. package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
  11. package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
  12. package/features/file-navigation/src/domain/navigation.mjs +12 -0
  13. package/features/file-navigation/src/entry.mjs +34 -0
  14. package/features/file-navigation/src/frontend/navigation.mjs +140 -0
  15. package/features/file-navigation/src/injection-source.mjs +9 -0
  16. package/features/file-navigation/src/navigation.css +16 -0
  17. package/features/file-navigation/vendor/lucide-LICENSE +43 -0
  18. package/features/markdown-reading/assets/book-open.svg +16 -0
  19. package/features/markdown-reading/codex-feature.json +27 -0
  20. package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
  21. package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
  22. package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
  23. package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
  24. package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
  25. package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
  26. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
  27. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
  28. package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
  29. package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
  30. package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
  31. package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
  32. package/features/markdown-reading/src/injection-source.mjs +43 -0
  33. package/features/markdown-reading/src/markdown-reading.mjs +82 -0
  34. package/features/markdown-reading/src/reading.css +346 -0
  35. package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
  36. package/features/model-slider/codex-feature.json +1 -0
  37. package/features/model-slider/src/backend/favorites.mjs +54 -0
  38. package/features/model-slider/src/domain/model-rules.mjs +6 -1
  39. package/features/model-slider/src/frontend/install.mjs +24 -3
  40. package/features/notes/assets/prompt-previews.json +26 -0
  41. package/features/notes/codex-feature.json +2 -1
  42. package/features/notes/src/backend/transfer.mjs +97 -0
  43. package/features/notes/src/frontend/entry.mjs +380 -10
  44. package/features/notes/src/frontend/icons.json +4 -1
  45. package/features/notes/src/frontend/styles.css +63 -0
  46. package/features/session-status/src/domain/session-stats.mjs +22 -8
  47. package/features/session-status/src/session-status.mjs +2 -2
  48. package/features/sites/src/backend/sites-service.mjs +20 -5
  49. package/features/sites/src/backend/transfer.mjs +43 -0
  50. package/package.json +11 -3
  51. package/packages/cli/src/config.mjs +4 -2
  52. package/packages/cli/src/control-server.mjs +23 -4
  53. package/packages/cli/src/data-transfer/service.mjs +42 -0
  54. package/packages/cli/src/runtime-manager.mjs +3 -0
  55. package/packages/runtime-host/src/codex-injection.mjs +1 -0
  56. package/packages/runtime-host/src/codex-runtime.mjs +11 -0
  57. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  58. package/packages/ui/src/native/theme.mjs +22 -1
  59. package/packages/ui/src/native/tokens.mjs +4 -0
@@ -0,0 +1,493 @@
1
+ // 保留原生逐行表格及选区;每张表的行共享横向位置,不移动正文节点。
2
+ export function installMarkdownTables(view, api) {
3
+ const { Decoration, ViewPlugin, StateEffect, Compartment, WidgetType } = api;
4
+ class EmptyCell extends WidgetType {
5
+ eq() {
6
+ return true;
7
+ }
8
+ toDOM() {
9
+ const span = document.createElement('span');
10
+ span.className = 'cm-markdown-table-cell mcx-reading-table-empty';
11
+ span.setAttribute('aria-label', '空单元格');
12
+ return span;
13
+ }
14
+ ignoreEvent() {
15
+ return false;
16
+ }
17
+ }
18
+ function slots(state, row) {
19
+ const result = [];
20
+ const delimiters = row.getChildren('TableDelimiter');
21
+ let start = row.from;
22
+ for (const delimiter of delimiters) {
23
+ if (start === row.from && !state.sliceDoc(start, delimiter.from).trim()) {
24
+ start = delimiter.to;
25
+ continue;
26
+ }
27
+ result.push({ from: start, to: delimiter.from });
28
+ start = delimiter.to;
29
+ }
30
+ if (state.sliceDoc(start, row.to).trim() || !delimiters.length) result.push({ from: start, to: row.to });
31
+ return result;
32
+ }
33
+ function build(editor) {
34
+ const ranges = [];
35
+ editor.state.tree.iterate({
36
+ from: editor.viewport.from,
37
+ to: editor.viewport.to,
38
+ enter(node) {
39
+ if (node.name !== 'Table') return;
40
+ const table = node.node;
41
+ const header = table.getChild('TableHeader');
42
+ if (!header) return false;
43
+ const rows = [header, ...table.getChildren('TableRow')];
44
+ const count = Math.max(...rows.map((row) => slots(editor.state, row).length));
45
+ if (!count) return false;
46
+ for (let index = 0; index < rows.length; index++) {
47
+ const row = rows[index];
48
+ if (row.to < editor.viewport.from || row.from > editor.viewport.to) continue;
49
+ ranges.push(
50
+ Decoration.line({
51
+ class: `mcx-reading-table-row horizontal-scroll-fade-mask horizontal-scroll-fade-mask-eased${index === rows.length - 1 ? ' mcx-reading-table-last' : ''}`,
52
+ attributes: {
53
+ 'data-mcx-reading-table': String(table.from),
54
+ },
55
+ }).range(editor.state.doc.lineAt(row.from).from),
56
+ );
57
+ const columns = slots(editor.state, row);
58
+ const cells = row.getChildren('TableCell');
59
+ for (let column = 0; column < count; column++) {
60
+ const slot = columns[column];
61
+ if (!slot || !cells.some((cell) => cell.from >= slot.from && cell.to <= slot.to)) {
62
+ ranges.push(
63
+ Decoration.widget({ widget: new EmptyCell(), side: 1 + column }).range(slot?.from ?? row.to),
64
+ );
65
+ }
66
+ }
67
+ }
68
+ return false;
69
+ },
70
+ });
71
+ return Decoration.set(ranges, true);
72
+ }
73
+ // 隐藏的原生 table 只负责浏览器列宽计算;正文仍由 CodeMirror 持有。
74
+ const measure = document.createElement('div');
75
+ measure.className = 'mcx-reading-table-measure';
76
+ measure.setAttribute('aria-hidden', 'true');
77
+ measure.inert = true;
78
+ view.dom.append(measure);
79
+ const layoutStyle = document.createElement('style');
80
+ const scope = `table-${Math.random().toString(36).slice(2)}`;
81
+ view.dom.dataset.mcxReadingTableScope = scope;
82
+ view.dom.append(layoutStyle);
83
+ let measuredDoc = null;
84
+ const tables = new Map();
85
+ const fitted = new Set();
86
+ let hovered = null;
87
+ function inline(state, node, target) {
88
+ let cursor = node.from;
89
+ for (let child = node.firstChild; child; child = child.nextSibling) {
90
+ target.append(document.createTextNode(state.sliceDoc(cursor, child.from)));
91
+ if (
92
+ !/^(EmphasisMark|CodeMark|LinkMark)$/.test(child.name) &&
93
+ !(child.name === 'URL' && node.name === 'Link' && state.sliceDoc(node.from, node.from + 1) === '[')
94
+ ) {
95
+ const tag = {
96
+ StrongEmphasis: 'strong',
97
+ Emphasis: 'em',
98
+ InlineCode: 'code',
99
+ Link: 'a',
100
+ Strikethrough: 's',
101
+ }[child.name];
102
+ if (child.name === 'Escape')
103
+ target.append(document.createTextNode(state.sliceDoc(child.from + 1, child.to)));
104
+ else if (child.name === 'Entity') {
105
+ const decoder = document.createElement('textarea');
106
+ decoder.innerHTML = state.sliceDoc(child.from, child.to);
107
+ target.append(document.createTextNode(decoder.value));
108
+ } else {
109
+ const element = tag ? document.createElement(tag) : target;
110
+ inline(state, child, element);
111
+ if (tag) target.append(element);
112
+ }
113
+ }
114
+ cursor = child.to;
115
+ }
116
+ target.append(document.createTextNode(state.sliceDoc(cursor, node.to)));
117
+ }
118
+ function measureColumns() {
119
+ if (measuredDoc !== view.state.doc) {
120
+ measure.replaceChildren();
121
+ tables.clear();
122
+ view.state.tree.iterate({
123
+ enter(node) {
124
+ if (node.name !== 'Table') return;
125
+ const header = node.node.getChild('TableHeader');
126
+ if (!header) return false;
127
+ const rows = [header, ...node.node.getChildren('TableRow')];
128
+ const count = Math.max(...rows.map((row) => slots(view.state, row).length));
129
+ const table = document.createElement('table');
130
+ const body = table.createTBody();
131
+ for (const [index, row] of rows.entries()) {
132
+ const tr = body.insertRow();
133
+ const columns = slots(view.state, row);
134
+ const cells = row.getChildren('TableCell');
135
+ for (let col = 0; col < count; col++) {
136
+ const td = document.createElement(index === 0 ? 'th' : 'td');
137
+ const slot = columns[col];
138
+ const cell = slot && cells.find((cell) => cell.from >= slot.from && cell.to <= slot.to);
139
+ if (cell) inline(view.state, cell, td);
140
+ tr.append(td);
141
+ }
142
+ }
143
+ table.dataset.to = String(node.node.to);
144
+ measure.append(table);
145
+ tables.set(String(node.from), table);
146
+ return false;
147
+ },
148
+ });
149
+ measuredDoc = view.state.doc;
150
+ }
151
+ const content = view.contentDOM;
152
+ const scroller = view.scrollDOM;
153
+ const padding = getComputedStyle(scroller);
154
+ const available = measure.clientWidth;
155
+ const inset = (scroller.clientWidth - available) / 2;
156
+ const breakout = Math.max(0, parseFloat(padding.paddingLeft) - inset);
157
+ for (const [id, table] of tables) {
158
+ table.style.inlineSize = fitted.has(id) ? '100%' : 'max-content';
159
+ for (const cell of table.querySelectorAll('td,th'))
160
+ cell.style.overflowWrap = fitted.has(id) ? 'anywhere' : 'break-word';
161
+ }
162
+ const widths = new Map(
163
+ [...tables].map(([id, table]) => [
164
+ id,
165
+ [...table.rows[0].cells].map((cell) => cell.getBoundingClientRect().width),
166
+ ]),
167
+ );
168
+ const rules = [];
169
+ for (const [id, columns] of widths) {
170
+ const selector = `[data-mcx-reading-table-scope="${scope}"] [data-mcx-reading-table="${id}"]`;
171
+ rules.push(
172
+ `${selector}{width:${available}px;margin-inline:${-breakout}px;${columns.reduce((a, b) => a + b, 0) <= available ? 'animation:none;mask:none;' : ''}}`,
173
+ );
174
+ columns.forEach((width, index) =>
175
+ rules.push(
176
+ `${selector} > .cm-markdown-table-cell:nth-child(${index + 1} of .cm-markdown-table-cell){width:${width}px;overflow-wrap:${fitted.has(id) ? 'anywhere' : 'break-word'}}`,
177
+ ),
178
+ );
179
+ }
180
+ const codeClasses = new Set();
181
+ for (const span of content.querySelectorAll('.cm-markdown-table-cell span[class]')) {
182
+ if (getComputedStyle(span).fontFamily.includes('monospace'))
183
+ for (const name of span.classList) codeClasses.add(name);
184
+ }
185
+ for (const name of codeClasses)
186
+ rules.push(
187
+ `[data-mcx-reading-table-scope="${scope}"] .cm-markdown-table-cell .${CSS.escape(name)}{font-size:.92em;font-weight:400;padding:1px 6px;border-radius:6px;background:color-mix(in srgb,var(--color-background-primary-ghost-hover) 60%,var(--color-text) 6%);box-decoration-break:clone;overflow-wrap:anywhere;word-break:break-word;}`,
188
+ );
189
+ const css = rules.join('\n');
190
+ if (layoutStyle.textContent !== css) {
191
+ layoutStyle.textContent = css;
192
+ view.requestMeasure();
193
+ }
194
+ }
195
+ const offsets = new Map();
196
+ const scrollbars = new Map();
197
+ const scrollLimits = new Map();
198
+ const scrollGeometryStyle = document.createElement('style');
199
+ view.dom.append(scrollGeometryStyle);
200
+ function updateScrollbars() {
201
+ const visible = new Set();
202
+ const frameRect = view.scrollDOM.getBoundingClientRect();
203
+ const scale = frameRect.height / view.scrollDOM.offsetHeight || 1;
204
+ for (const last of view.contentDOM.querySelectorAll('.mcx-reading-table-last')) {
205
+ const id = last.dataset.mcxReadingTable;
206
+ if (last.scrollWidth <= last.clientWidth + 1) continue;
207
+ visible.add(id);
208
+ let bar = scrollbars.get(id);
209
+ if (!bar) {
210
+ bar = document.createElement('div');
211
+ bar.className = 'mcx-reading-table-scrollbar';
212
+ bar.dataset.mcxReadingTableScrollbar = id;
213
+ bar.setAttribute('aria-label', '表格横向滚动');
214
+ bar.tabIndex = 0;
215
+ bar.setAttribute('role', 'scrollbar');
216
+ bar.setAttribute('aria-orientation', 'horizontal');
217
+ bar.setAttribute('aria-valuemin', '0');
218
+ bar.append(document.createElement('div'));
219
+ bar.addEventListener(
220
+ 'wheel',
221
+ (event) => {
222
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? view.scrollDOM.clientHeight : 1;
223
+ event.preventDefault();
224
+ if (event.deltaX || event.shiftKey) bar.scrollLeft += (event.deltaX || event.deltaY) * unit;
225
+ else view.scrollDOM.scrollTop += event.deltaY * unit;
226
+ },
227
+ { passive: false },
228
+ );
229
+ bar.addEventListener('keydown', (event) => {
230
+ const targets = {
231
+ ArrowLeft: bar.scrollLeft - 40,
232
+ ArrowRight: bar.scrollLeft + 40,
233
+ PageUp: bar.scrollLeft - bar.clientWidth,
234
+ PageDown: bar.scrollLeft + bar.clientWidth,
235
+ Home: 0,
236
+ End: bar.scrollWidth,
237
+ };
238
+ if (event.key in targets) {
239
+ event.preventDefault();
240
+ bar.scrollLeft = targets[event.key];
241
+ }
242
+ });
243
+ bar.addEventListener('scroll', () => {
244
+ offsets.set(id, bar.scrollLeft);
245
+ for (const row of view.contentDOM.querySelectorAll('[data-mcx-reading-table]'))
246
+ if (row.dataset.mcxReadingTable === id) row.scrollLeft = bar.scrollLeft;
247
+ schedule();
248
+ view.requestMeasure();
249
+ });
250
+ view.scrollDOM.append(bar);
251
+ scrollbars.set(id, bar);
252
+ }
253
+ const rect = last.getBoundingClientRect();
254
+ bar.dataset.visible = String(hovered === id);
255
+ Object.assign(bar.style, {
256
+ left: `${(rect.left - frameRect.left) / scale}px`,
257
+ top: `${(rect.bottom - frameRect.top) / scale - parseFloat(getComputedStyle(last).paddingBottom) + view.scrollDOM.scrollTop}px`,
258
+ width: `${last.clientWidth}px`,
259
+ });
260
+ bar.firstElementChild.style.width = `${last.scrollWidth}px`;
261
+ bar.scrollLeft = offsets.get(id) ?? 0;
262
+ bar.setAttribute('aria-valuemax', String(bar.scrollWidth - bar.clientWidth));
263
+ bar.setAttribute('aria-valuenow', String(Math.round(bar.scrollLeft)));
264
+ }
265
+ for (const [id, bar] of scrollbars)
266
+ if (!visible.has(id)) {
267
+ bar.remove();
268
+ scrollbars.delete(id);
269
+ }
270
+ }
271
+ let frame = null;
272
+ let revealCursor = false;
273
+ let geometryRows = [],
274
+ geometryColumns = '';
275
+ const restore = () => {
276
+ frame = null;
277
+ if (view.destroyed) return;
278
+ measureColumns();
279
+ const currentRows = [...view.contentDOM.querySelectorAll('[data-mcx-reading-table]')];
280
+ const geometryChanged =
281
+ geometryColumns !== layoutStyle.textContent ||
282
+ currentRows.length !== geometryRows.length ||
283
+ currentRows.some((row, index) => row !== geometryRows[index]);
284
+ if (geometryChanged) {
285
+ geometryRows = currentRows;
286
+ geometryColumns = layoutStyle.textContent;
287
+ scrollGeometryStyle.textContent = '';
288
+ const groups = new Map();
289
+ for (const row of view.contentDOM.querySelectorAll('[data-mcx-reading-table]')) {
290
+ const id = row.dataset.mcxReadingTable;
291
+ if (!groups.has(id)) groups.set(id, []);
292
+ groups.get(id).push(row);
293
+ }
294
+ const scrollRules = [];
295
+ scrollLimits.clear();
296
+ for (const [id, rows] of groups) {
297
+ const width = Math.max(...rows.map((row) => row.scrollWidth));
298
+ const limit = Math.max(0, width - rows[0].clientWidth);
299
+ scrollLimits.set(id, limit > 1 ? limit : 0);
300
+ offsets.set(id, Math.min(offsets.get(id) ?? 0, limit > 1 ? limit : 0));
301
+ scrollRules.push(
302
+ `[data-mcx-reading-table-scope="${scope}"] [data-mcx-reading-table="${id}"]{--mcx-table-scroll-width:${width}px}`,
303
+ );
304
+ }
305
+ scrollGeometryStyle.textContent = scrollRules.join('\n');
306
+ }
307
+ for (const row of view.contentDOM.querySelectorAll('[data-mcx-reading-table]')) {
308
+ const offset = offsets.get(row.dataset.mcxReadingTable) ?? 0;
309
+ if (row.scrollLeft !== offset) row.scrollLeft = offset;
310
+ }
311
+ updateScrollbars();
312
+ if (controller.renderActions) {
313
+ const frameRect = view.scrollDOM.getBoundingClientRect(),
314
+ items = [];
315
+ const scale = frameRect.height / view.scrollDOM.offsetHeight || 1;
316
+ for (const [id, table] of tables) {
317
+ const rows = [...view.contentDOM.querySelectorAll('[data-mcx-reading-table]')].filter(
318
+ (row) => row.dataset.mcxReadingTable === id,
319
+ );
320
+ if (!rows.length) continue;
321
+ const first = rows[0].getBoundingClientRect();
322
+ const headerInset = parseFloat(getComputedStyle(rows[0]).paddingTop);
323
+ items.push({
324
+ id,
325
+ fit: fitted.has(id),
326
+ overflow: (scrollLimits.get(id) ?? 0) > 0,
327
+ visible: hovered === id,
328
+ left: (first.left - frameRect.left) / scale,
329
+ top: (first.top - frameRect.top) / scale + view.scrollDOM.scrollTop + headerInset - 40,
330
+ width: first.width / scale,
331
+ source: view.state.sliceDoc(Number(id), Number(table.dataset.to)),
332
+ });
333
+ }
334
+ controller.renderActions(items);
335
+ }
336
+ const shouldReveal = revealCursor;
337
+ revealCursor = false;
338
+ if (shouldReveal && view.hasFocus && view.state.selection.main.empty) {
339
+ const pos = view.state.selection.main.head;
340
+ const dom = view.domAtPos(pos).node;
341
+ const row = (dom.nodeType === 1 ? dom : dom.parentElement)?.closest('[data-mcx-reading-table]');
342
+ const cursor = view.coordsAtPos(pos);
343
+ if (row && cursor) {
344
+ const rect = row.getBoundingClientRect();
345
+ const scale = rect.width / row.offsetWidth;
346
+ const delta =
347
+ cursor.left < rect.left + 12
348
+ ? cursor.left - rect.left - 12
349
+ : cursor.right > rect.right - 12
350
+ ? cursor.right - rect.right + 12
351
+ : 0;
352
+ if (delta) {
353
+ const offset = Math.max(
354
+ 0,
355
+ Math.min(row.scrollWidth - row.clientWidth, row.scrollLeft + delta / scale),
356
+ );
357
+ offsets.set(row.dataset.mcxReadingTable, offset);
358
+ for (const sibling of view.contentDOM.querySelectorAll('[data-mcx-reading-table]'))
359
+ if (sibling.dataset.mcxReadingTable === row.dataset.mcxReadingTable) sibling.scrollLeft = offset;
360
+ view.requestMeasure();
361
+ }
362
+ }
363
+ }
364
+ };
365
+ const schedule = () => {
366
+ if (frame === null) frame = requestAnimationFrame(restore);
367
+ };
368
+ const resize = new ResizeObserver(schedule);
369
+ resize.observe(view.dom);
370
+ // 阻止命中行先由 compositor 滚动;一次输入同步提交全表偏移。
371
+ const onWheel = (event) => {
372
+ if (event.ctrlKey || !event.cancelable) return;
373
+ const row = event.target instanceof Element ? event.target.closest('[data-mcx-reading-table]') : null;
374
+ const delta = event.deltaX || (event.shiftKey ? event.deltaY : 0);
375
+ if (!row || !delta) return;
376
+ event.preventDefault();
377
+ event.stopPropagation();
378
+ const id = row.dataset.mcxReadingTable;
379
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? row.clientWidth : 1;
380
+ const offset = Math.max(0, Math.min(scrollLimits.get(id) ?? 0, (offsets.get(id) ?? 0) + delta * unit));
381
+ offsets.set(id, offset);
382
+ for (const sibling of view.contentDOM.querySelectorAll('[data-mcx-reading-table]')) {
383
+ if (sibling.dataset.mcxReadingTable === id) sibling.scrollLeft = offset;
384
+ }
385
+ const bar = scrollbars.get(id);
386
+ if (bar) bar.scrollLeft = offset;
387
+ if (event.deltaX && event.deltaY && !event.shiftKey) view.scrollDOM.scrollTop += event.deltaY * unit;
388
+ schedule();
389
+ view.requestMeasure();
390
+ };
391
+ const onScroll = (event) => {
392
+ const row = event.target;
393
+ if (!row.matches?.('[data-mcx-reading-table]')) return;
394
+ const id = row.dataset.mcxReadingTable;
395
+ if (!scrollLimits.get(id)) {
396
+ offsets.set(id, 0);
397
+ if (row.scrollLeft) row.scrollLeft = 0;
398
+ return;
399
+ }
400
+ if (Math.abs((offsets.get(id) ?? 0) - row.scrollLeft) < 0.5) return;
401
+ offsets.set(id, row.scrollLeft);
402
+ const bar = scrollbars.get(id);
403
+ if (bar) bar.scrollLeft = row.scrollLeft;
404
+ for (const sibling of view.contentDOM.querySelectorAll('[data-mcx-reading-table]'))
405
+ if (sibling.dataset.mcxReadingTable === id && sibling !== row) sibling.scrollLeft = row.scrollLeft;
406
+ view.requestMeasure();
407
+ };
408
+ const tableId = (target) =>
409
+ target instanceof Element
410
+ ? (target
411
+ .closest(
412
+ '[data-mcx-reading-table],[data-mcx-reading-table-action-id],[data-mcx-reading-table-scrollbar]',
413
+ )
414
+ ?.getAttribute('data-mcx-reading-table') ??
415
+ target.closest('[data-mcx-reading-table-action-id]')?.dataset.mcxReadingTableActionId ??
416
+ target.closest('[data-mcx-reading-table-scrollbar]')?.dataset.mcxReadingTableScrollbar)
417
+ : null;
418
+ const onHover = (event) => {
419
+ const id = tableId(event.type === 'pointerout' ? event.relatedTarget : event.target);
420
+ if (hovered !== id) {
421
+ hovered = id;
422
+ schedule();
423
+ }
424
+ };
425
+ view.dom.addEventListener('pointerover', onHover);
426
+ view.dom.addEventListener('pointerout', onHover);
427
+ view.dom.addEventListener('focusin', schedule);
428
+ view.dom.addEventListener('focusout', schedule);
429
+ view.contentDOM.addEventListener('scroll', onScroll, true);
430
+ view.contentDOM.addEventListener('wheel', onWheel, { capture: true, passive: false });
431
+ view.scrollDOM.addEventListener('scroll', schedule, { passive: true });
432
+ const plugin = ViewPlugin.define(
433
+ (editor) => ({
434
+ decorations: build(editor),
435
+ update(update) {
436
+ if (update.docChanged) {
437
+ offsets.clear();
438
+ fitted.clear();
439
+ geometryRows = [];
440
+ }
441
+ if (update.docChanged || update.startState.tree !== update.state.tree) measuredDoc = null;
442
+ if (update.docChanged || update.viewportChanged || update.startState.tree !== update.state.tree)
443
+ this.decorations = build(update.view);
444
+ if (
445
+ (update.selectionSet && update.transactions.some((transaction) => transaction.scrollIntoView)) ||
446
+ update.focusChanged
447
+ )
448
+ revealCursor = true;
449
+ if (update.docChanged || update.viewportChanged || update.geometryChanged || revealCursor) schedule();
450
+ },
451
+ }),
452
+ { decorations: (value) => value.decorations },
453
+ );
454
+ const compartment = new Compartment();
455
+ view.dispatch({ effects: StateEffect.appendConfig.of(compartment.of(plugin)) });
456
+ const controller = {
457
+ schedule,
458
+ renderActions: null,
459
+ toggle(id) {
460
+ if (fitted.has(id)) fitted.delete(id);
461
+ else fitted.add(id);
462
+ offsets.delete(id);
463
+ schedule();
464
+ view.requestMeasure();
465
+ },
466
+ };
467
+ schedule();
468
+ const dispose = () => {
469
+ view.dom.removeEventListener('pointerover', onHover);
470
+ view.dom.removeEventListener('pointerout', onHover);
471
+ view.dom.removeEventListener('focusin', schedule);
472
+ view.dom.removeEventListener('focusout', schedule);
473
+ view.contentDOM.removeEventListener('scroll', onScroll, true);
474
+ view.contentDOM.removeEventListener('wheel', onWheel, true);
475
+ view.scrollDOM.removeEventListener('scroll', schedule);
476
+ for (const bar of scrollbars.values()) bar.remove();
477
+ scrollbars.clear();
478
+ scrollLimits.clear();
479
+ scrollGeometryStyle.remove();
480
+ if (frame !== null) cancelAnimationFrame(frame);
481
+ for (const row of view.contentDOM.querySelectorAll('[data-mcx-reading-table]')) {
482
+ row.scrollLeft = 0;
483
+ }
484
+ if (!view.destroyed) view.dispatch({ effects: compartment.reconfigure([]) });
485
+ offsets.clear();
486
+ resize.disconnect();
487
+ measure.remove();
488
+ layoutStyle.remove();
489
+ delete view.dom.dataset.mcxReadingTableScope;
490
+ };
491
+ dispose.controller = controller;
492
+ return dispose;
493
+ }
@@ -0,0 +1,131 @@
1
+ // 直接导入原生 Marker、目录浮层和 Tooltip;仅按源码调用关系发现公开角色。
2
+ export async function loadNativeMarkdownToc() {
3
+ const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(
4
+ (n) => n.href || n.src,
5
+ );
6
+ const initialUrl = urls.find((url) => /\/app-initial-[^/]+\.js/.test(url));
7
+ const mainUrl = urls.find((url) => /\/app-main-[^/]+\.js/.test(url));
8
+ if (!initialUrl || !mainUrl) throw new Error('native-page-toc-modules-unavailable');
9
+ const read = async (url) => {
10
+ const response = await fetch(url);
11
+ if (!response.ok) throw new Error('native-page-toc-source-unavailable');
12
+ return response.text();
13
+ };
14
+ const [initial, main] = await Promise.all([read(initialUrl), read(mainUrl)]);
15
+ const candidates = [
16
+ ...new Set([
17
+ ...urls.filter((url) => /\/content-[^/]+\.js/.test(url)),
18
+ ...[...initial.matchAll(/["']\.\/((?:content)-[\w-]+\.js)["']/g)].map(
19
+ (m) => new URL(m[1], initialUrl).href,
20
+ ),
21
+ ]),
22
+ ];
23
+ let source, sourceUrl;
24
+ for (const url of candidates) {
25
+ const value = await read(url);
26
+ if (
27
+ value.includes('data-page-heading-id') &&
28
+ value.includes('data-heading-preview') &&
29
+ value.includes('PageEditorContent')
30
+ ) {
31
+ source = value;
32
+ sourceUrl = url;
33
+ break;
34
+ }
35
+ }
36
+ if (!source) throw new Error('native-page-toc-components-unavailable');
37
+ function bodyMatching(pattern) {
38
+ const starts = [...source.matchAll(/function [\w$]+\(/g)].map((m) => m.index);
39
+ const matches = starts
40
+ .map((start, index) => source.slice(start, starts[index + 1] ?? source.length))
41
+ .filter((body) => pattern.test(body));
42
+ if (matches.length !== 1) throw new Error('native-page-toc-component-contract-changed');
43
+ // 产物在函数之后声明闭包变量;只取原始函数体。
44
+ const body = matches[0],
45
+ end = body.lastIndexOf('}');
46
+ return body.slice(0, end + 1);
47
+ }
48
+ const railBody = bodyMatching(/data-floating-navigation-rail-list.*data-scrubbing/);
49
+ const listBody = bodyMatching(/data-heading-preview.*wrap-anywhere/);
50
+ const imports = new Map();
51
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
52
+ for (const entry of match[1].split(',')) {
53
+ const parts = entry.trim().split(/\s+as\s+/);
54
+ imports.set(parts.at(-1), { name: parts[0], url: new URL(match[2], sourceUrl).href });
55
+ }
56
+ const modules = new Map();
57
+ async function binding(local, imported = imports) {
58
+ const item = imported.get(local);
59
+ if (!item) throw new Error('native-page-toc-dependency-contract-changed');
60
+ if (!modules.has(item.url)) modules.set(item.url, import(item.url));
61
+ return (await modules.get(item.url))[item.name];
62
+ }
63
+ const react = railBody.match(/\(0,([\w$]+)\.useState\)/)?.[1];
64
+ const reactFactory = react && source.match(new RegExp(`\\b${react}=([\\w$]+)\\(\\)`))?.[1];
65
+ if (!reactFactory) throw new Error('native-page-toc-react-unavailable');
66
+ const React = (await binding(reactFactory))();
67
+ const tooltipLocal = source.match(
68
+ /jsx\)\(([\w$]+),\{align:`center`,delayOpen:!0,interactive:!0,side:`right`,sideOffset:0,skipDelayKey:`page-table-of-contents`/,
69
+ )?.[1];
70
+ const Tooltip = await binding(tooltipLocal);
71
+ const layoutUrl = [...imports.values()].find((item) =>
72
+ item.url.includes('/floating-navigation-rail-layout-'),
73
+ )?.url;
74
+ if (!layoutUrl) throw new Error('native-page-toc-layout-unavailable');
75
+ const layout = await import(layoutUrl);
76
+ for (const value of Object.values(layout))
77
+ if (typeof value === 'function' && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
78
+ const rootOwner = main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
79
+ const rootFactory = rootOwner && main.match(new RegExp(`${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
80
+ const mainImports = new Map();
81
+ for (const match of main.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
82
+ for (const entry of match[1].split(',')) {
83
+ const parts = entry.trim().split(/\s+as\s+/);
84
+ mainImports.set(parts.at(-1), { name: parts[0], url: new URL(match[2], mainUrl).href });
85
+ }
86
+ const ReactDOM = (await binding(rootFactory, mainImports))();
87
+ const css = initial.match(/["']\.\/((?:floating-navigation-rail-layout)-[\w-]+\.css)["']/)?.[1];
88
+ if (!css) throw new Error('native-page-toc-styles-unavailable');
89
+ const cssUrl = new URL(css, initialUrl).href;
90
+ if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((n) => n.href === cssUrl)) {
91
+ const link = document.createElement('link');
92
+ link.rel = 'stylesheet';
93
+ link.href = cssUrl;
94
+ document.head.append(link);
95
+ await new Promise((resolve, reject) => {
96
+ link.onload = resolve;
97
+ link.onerror = () => reject(new Error('native-page-toc-styles-unavailable'));
98
+ });
99
+ }
100
+ const component = (predicate) => {
101
+ const matches = Object.values(layout).filter(
102
+ (value) => typeof value === 'function' && predicate(String(value)),
103
+ );
104
+ if (matches.length !== 1) throw new Error('native-page-toc-layout-contract-changed');
105
+ return matches[0];
106
+ };
107
+ const Marker = component((body) => body.includes('hierarchyLevel'));
108
+ const Panel = component((body) => body.includes('shadow-xl-spread'));
109
+ const requireClass = (body, pattern) => {
110
+ const value = body.match(pattern)?.[1];
111
+ if (!value) throw new Error('native-page-toc-class-contract-changed');
112
+ return value;
113
+ };
114
+ const classes = {
115
+ rail: requireClass(railBody, /ref:[\w$]+,className:`([^`]+)`,"data-floating-navigation-rail-list"/),
116
+ railButton: requireClass(railBody, /className:`([^`]+)`,"aria-current"/),
117
+ menu: requireClass(listBody, /ref:[\w$]+,className:`([^`]+)`,children:/),
118
+ menuButton: requireClass(listBody, /className:[\w$]+\(`([^`]+)`/),
119
+ nav: requireClass(source, /className:`(absolute top-1\/2 left-1\.5[^`]+)`/),
120
+ };
121
+ const indentName = listBody.match(/`block`,([\w$]+)\[e\.level\]/)?.[1];
122
+ const indentValue = indentName && source.match(new RegExp(`\\b${indentName}=(\\[[^\\]]+\\])`))?.[1];
123
+ if (!indentValue || !/^\[(?:void 0|`ms-\d+`|,)+\]$/.test(indentValue))
124
+ throw new Error('native-page-toc-indent-contract-changed');
125
+ const indentation = indentValue
126
+ .slice(1, -1)
127
+ .split(',')
128
+ .map((value) => (value === 'void 0' ? '' : value.slice(1, -1)));
129
+ if (!ReactDOM.createRoot || !React.createElement) throw new Error('native-page-toc-react-unavailable');
130
+ return { Marker, Panel, Tooltip, React, ReactDOM, classes, indentation };
131
+ }