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.
- package/README.md +20 -8
- package/apps/dashboard/app.js +4 -4
- package/apps/dashboard/assets/data-transfer.js +129 -0
- package/apps/dashboard/assets/download.svg +15 -0
- package/apps/dashboard/assets/upload.svg +15 -0
- package/apps/dashboard/index.html +10 -0
- package/apps/dashboard/styles.css +50 -3
- package/features/file-navigation/assets/book-open.svg +16 -0
- package/features/file-navigation/assets/folder-tree.svg +16 -0
- package/features/file-navigation/codex-feature.json +27 -0
- package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
- package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
- package/features/file-navigation/src/domain/navigation.mjs +12 -0
- package/features/file-navigation/src/entry.mjs +34 -0
- package/features/file-navigation/src/frontend/navigation.mjs +140 -0
- package/features/file-navigation/src/injection-source.mjs +9 -0
- package/features/file-navigation/src/navigation.css +16 -0
- package/features/file-navigation/vendor/lucide-LICENSE +43 -0
- package/features/hide-upsell-banner/codex-feature.json +4 -4
- package/features/images/codex-feature.json +4 -4
- package/features/images/src/adapters/codex/native-image-modules.mjs +13 -2
- package/features/images/src/library-prompt-source.mjs +2 -2
- package/features/markdown-reading/assets/book-open.svg +16 -0
- package/features/markdown-reading/codex-feature.json +27 -0
- package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
- package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
- package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
- package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
- package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
- package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
- package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
- package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
- package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
- package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
- package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
- package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
- package/features/markdown-reading/src/injection-source.mjs +43 -0
- package/features/markdown-reading/src/markdown-reading.mjs +82 -0
- package/features/markdown-reading/src/reading.css +346 -0
- package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
- package/features/model-slider/codex-feature.json +5 -4
- package/features/model-slider/src/backend/favorites.mjs +54 -0
- package/features/model-slider/src/domain/model-rules.mjs +6 -1
- package/features/model-slider/src/frontend/install.mjs +24 -3
- package/features/notes/assets/prompt-previews.json +26 -0
- package/features/notes/codex-feature.json +6 -5
- package/features/notes/src/backend/transfer.mjs +97 -0
- package/features/notes/src/frontend/entry.mjs +380 -10
- package/features/notes/src/frontend/icons.json +4 -1
- package/features/notes/src/frontend/styles.css +63 -0
- package/features/session-status/codex-feature.json +6 -3
- package/features/session-status/src/domain/session-stats.mjs +22 -8
- package/features/session-status/src/session-status.mjs +2 -2
- package/features/sidebar-focus/codex-feature.json +2 -2
- package/features/sidebar-focus/src/sidebar-focus.mjs +13 -5
- package/features/sites/codex-feature.json +4 -4
- package/features/sites/src/backend/sites-service.mjs +20 -5
- package/features/sites/src/backend/transfer.mjs +43 -0
- package/features/usage-meter/codex-feature.json +6 -3
- package/package.json +11 -3
- package/packages/cli/src/config.mjs +4 -2
- package/packages/cli/src/control-server.mjs +34 -10
- package/packages/cli/src/data-transfer/service.mjs +42 -0
- package/packages/cli/src/runtime-manager.mjs +3 -0
- package/packages/runtime-host/src/codex-injection.mjs +2 -1
- package/packages/runtime-host/src/codex-runtime.mjs +24 -1
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/src/native/theme.mjs +23 -1
- package/packages/ui/src/native/tokens.mjs +4 -4
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { captureMarkdownProviders } from './providers.mjs';
|
|
2
|
+
|
|
3
|
+
// Markdown 负责标题与导航;原生组件负责短线、动效、浮层材质与悬浮定位。
|
|
4
|
+
export function installMarkdownToc(view, api, native, codex) {
|
|
5
|
+
const { React, ReactDOM, Marker, Panel, Tooltip, classes, indentation } = native;
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
const compartment = new api.Compartment();
|
|
8
|
+
const controller = new AbortController(),
|
|
9
|
+
{ signal } = controller;
|
|
10
|
+
const mount = document.createElement('div');
|
|
11
|
+
const panel = view.dom.closest('[data-app-shell-tab-panel-controller]');
|
|
12
|
+
mount.dataset.mcxReadingToc = '';
|
|
13
|
+
mount.style.display = 'contents';
|
|
14
|
+
view.dom.append(mount);
|
|
15
|
+
const withProviders = captureMarkdownProviders(view.dom, codex);
|
|
16
|
+
const root = ReactDOM.createRoot(mount);
|
|
17
|
+
let headings = [],
|
|
18
|
+
active = null,
|
|
19
|
+
visible = false,
|
|
20
|
+
frame = null,
|
|
21
|
+
navigationFrame = null,
|
|
22
|
+
navigating = false,
|
|
23
|
+
scrollTimer = null;
|
|
24
|
+
const scrollBehavior = view.scrollDOM.style.scrollBehavior;
|
|
25
|
+
function restoreScroll() {
|
|
26
|
+
clearTimeout(scrollTimer);
|
|
27
|
+
if (navigationFrame !== null) cancelAnimationFrame(navigationFrame);
|
|
28
|
+
navigationFrame = null;
|
|
29
|
+
view.scrollDOM.style.scrollBehavior = scrollBehavior;
|
|
30
|
+
}
|
|
31
|
+
function navigate(heading, behavior) {
|
|
32
|
+
restoreScroll();
|
|
33
|
+
active = heading.id;
|
|
34
|
+
navigating = true;
|
|
35
|
+
render();
|
|
36
|
+
const start = view.scrollDOM.scrollTop;
|
|
37
|
+
view.scrollDOM.style.scrollBehavior = 'auto';
|
|
38
|
+
view.dispatch({ effects: view.constructor.scrollIntoView(heading.from, { y: 'start', yMargin: 24 }) });
|
|
39
|
+
if (behavior !== 'instant' && !matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
40
|
+
navigationFrame = requestAnimationFrame(() => {
|
|
41
|
+
navigationFrame = requestAnimationFrame(() => {
|
|
42
|
+
navigationFrame = null;
|
|
43
|
+
const destination = view.scrollDOM.scrollTop;
|
|
44
|
+
view.scrollDOM.scrollTo({ top: start, behavior: 'instant' });
|
|
45
|
+
view.scrollDOM.scrollTo({ top: destination, behavior: 'smooth' });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
scrollTimer = setTimeout(() => {
|
|
50
|
+
restoreScroll();
|
|
51
|
+
navigating = false;
|
|
52
|
+
schedule();
|
|
53
|
+
}, 1200);
|
|
54
|
+
}
|
|
55
|
+
function reveal(container, item) {
|
|
56
|
+
if (!container || !item) return;
|
|
57
|
+
let top = item.offsetTop;
|
|
58
|
+
for (
|
|
59
|
+
let parent = item.offsetParent;
|
|
60
|
+
parent instanceof HTMLElement && parent !== container && parent !== container.offsetParent;
|
|
61
|
+
parent = parent.offsetParent
|
|
62
|
+
)
|
|
63
|
+
top += parent.offsetTop;
|
|
64
|
+
if (top < container.scrollTop) container.scrollTop = top;
|
|
65
|
+
else if (top + item.offsetHeight > container.scrollTop + container.clientHeight)
|
|
66
|
+
container.scrollTop = top + item.offsetHeight - container.clientHeight + 1;
|
|
67
|
+
}
|
|
68
|
+
function Contents({ headings, active }) {
|
|
69
|
+
const [preview, setPreview] = React.useState(null),
|
|
70
|
+
[scrub, setScrub] = React.useState(null);
|
|
71
|
+
const rail = React.useRef(null),
|
|
72
|
+
list = React.useRef(null),
|
|
73
|
+
pointer = React.useRef(null),
|
|
74
|
+
dragged = React.useRef(false);
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (!pointer.current)
|
|
77
|
+
reveal(rail.current, rail.current?.querySelector(`[data-page-heading-id="${preview ?? active}"]`));
|
|
78
|
+
}, [preview, active]);
|
|
79
|
+
function end(event) {
|
|
80
|
+
const p = pointer.current;
|
|
81
|
+
if (p?.pointerId !== event.pointerId) return;
|
|
82
|
+
pointer.current = null;
|
|
83
|
+
setScrub(null);
|
|
84
|
+
if (p.target.hasPointerCapture?.(event.pointerId)) p.target.releasePointerCapture(event.pointerId);
|
|
85
|
+
if (event.type === 'pointercancel' || event.type === 'lostpointercapture') dragged.current = false;
|
|
86
|
+
}
|
|
87
|
+
function move(event) {
|
|
88
|
+
const p = pointer.current;
|
|
89
|
+
if (!p || p.pointerId !== event.pointerId) return;
|
|
90
|
+
if (event.buttons % 2 === 0) {
|
|
91
|
+
end(event);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const element = event.currentTarget,
|
|
95
|
+
rect = element.getBoundingClientRect();
|
|
96
|
+
const button = document
|
|
97
|
+
.elementFromPoint(
|
|
98
|
+
rect.left + rect.width / 2,
|
|
99
|
+
Math.max(rect.top, Math.min(event.clientY, rect.bottom - 1)),
|
|
100
|
+
)
|
|
101
|
+
?.closest('[data-page-heading-id]');
|
|
102
|
+
if (!button || !element.contains(button)) return;
|
|
103
|
+
const heading = headings.find((h) => h.id === button.dataset.pageHeadingId);
|
|
104
|
+
if (heading && heading.id !== p.headingId) {
|
|
105
|
+
p.headingId = heading.id;
|
|
106
|
+
dragged.current = true;
|
|
107
|
+
setScrub(heading.id);
|
|
108
|
+
setPreview(heading.id);
|
|
109
|
+
navigate(heading, 'instant');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const railTree = h(
|
|
113
|
+
'div',
|
|
114
|
+
{
|
|
115
|
+
ref: rail,
|
|
116
|
+
className: classes.rail,
|
|
117
|
+
'data-floating-navigation-rail-list': true,
|
|
118
|
+
'data-scrubbing': scrub != null || undefined,
|
|
119
|
+
onPointerMove: move,
|
|
120
|
+
onPointerUp: end,
|
|
121
|
+
onPointerCancel: end,
|
|
122
|
+
onLostPointerCapture: end,
|
|
123
|
+
},
|
|
124
|
+
headings.map((heading, index) =>
|
|
125
|
+
h(
|
|
126
|
+
'button',
|
|
127
|
+
{
|
|
128
|
+
key: heading.id,
|
|
129
|
+
type: 'button',
|
|
130
|
+
className: classes.railButton,
|
|
131
|
+
'aria-current': active === heading.id ? 'true' : undefined,
|
|
132
|
+
'aria-label': `Jump to heading ${index + 1}: ${heading.title}`,
|
|
133
|
+
'data-page-heading-id': heading.id,
|
|
134
|
+
'data-scrub-target': (scrub ?? preview) === heading.id || undefined,
|
|
135
|
+
onPointerDown: (event) => {
|
|
136
|
+
if (event.button === 0 && !pointer.current) {
|
|
137
|
+
dragged.current = false;
|
|
138
|
+
pointer.current = {
|
|
139
|
+
pointerId: event.pointerId,
|
|
140
|
+
target: event.currentTarget,
|
|
141
|
+
headingId: heading.id,
|
|
142
|
+
};
|
|
143
|
+
setScrub(heading.id);
|
|
144
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
onClick: (event) => {
|
|
148
|
+
if (dragged.current && event.detail !== 0) {
|
|
149
|
+
dragged.current = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
navigate(heading);
|
|
153
|
+
},
|
|
154
|
+
onPointerEnter: () => setPreview(heading.id),
|
|
155
|
+
onFocus: () => setPreview(heading.id),
|
|
156
|
+
},
|
|
157
|
+
h(Marker, { hierarchyLevel: Math.min(heading.level, 3) }),
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
const current = preview ?? active;
|
|
162
|
+
const listTree = h(
|
|
163
|
+
Panel,
|
|
164
|
+
{
|
|
165
|
+
ref: (element) => {
|
|
166
|
+
list.current = element;
|
|
167
|
+
reveal(element, element?.querySelector('[data-heading-preview]'));
|
|
168
|
+
},
|
|
169
|
+
className: classes.menu,
|
|
170
|
+
},
|
|
171
|
+
headings.map((heading) =>
|
|
172
|
+
h(
|
|
173
|
+
'button',
|
|
174
|
+
{
|
|
175
|
+
key: heading.id,
|
|
176
|
+
type: 'button',
|
|
177
|
+
ref: (element) => {
|
|
178
|
+
if (current === heading.id) reveal(list.current, element);
|
|
179
|
+
},
|
|
180
|
+
'aria-current': active === heading.id ? 'location' : undefined,
|
|
181
|
+
'data-heading-preview': current === heading.id ? '' : undefined,
|
|
182
|
+
className: `${classes.menuButton} ${active === heading.id ? 'text-default' : 'text-secondary'} ${preview === heading.id ? 'bg-background-primary-ghost-hover/50' : ''}`,
|
|
183
|
+
onClick: () => navigate(heading),
|
|
184
|
+
onPointerEnter: () => setPreview(heading.id),
|
|
185
|
+
onFocus: () => setPreview(heading.id),
|
|
186
|
+
},
|
|
187
|
+
h('span', { className: `block ${indentation[Math.min(heading.level, 3)] ?? ''}` }, heading.title),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
return h(
|
|
192
|
+
'nav',
|
|
193
|
+
{
|
|
194
|
+
'aria-label': '文档目录',
|
|
195
|
+
className: classes.nav,
|
|
196
|
+
style: { top: 'var(--mcx-reading-toc-center, 50%)' },
|
|
197
|
+
},
|
|
198
|
+
h(
|
|
199
|
+
Tooltip,
|
|
200
|
+
{
|
|
201
|
+
align: 'center',
|
|
202
|
+
delayOpen: true,
|
|
203
|
+
interactive: true,
|
|
204
|
+
side: 'right',
|
|
205
|
+
sideOffset: 0,
|
|
206
|
+
skipDelayKey: 'page-table-of-contents',
|
|
207
|
+
tooltipMaxWidth: 'min(20rem, calc(100vw - 16px))',
|
|
208
|
+
variant: 'floating-navigation-rail',
|
|
209
|
+
onPointerLeave: () => {
|
|
210
|
+
if (!list.current) setPreview(null);
|
|
211
|
+
},
|
|
212
|
+
onOpenChange: (open) => {
|
|
213
|
+
if (!open) setPreview(null);
|
|
214
|
+
},
|
|
215
|
+
tooltipContent: listTree,
|
|
216
|
+
},
|
|
217
|
+
railTree,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
function render() {
|
|
222
|
+
let tree = visible ? h(Contents, { headings, active }) : null;
|
|
223
|
+
root.render(withProviders(React, tree));
|
|
224
|
+
}
|
|
225
|
+
function build() {
|
|
226
|
+
const next = [],
|
|
227
|
+
text = view.state.doc.toString();
|
|
228
|
+
const frontmatter = /^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)(?:\r?\n|$)/.exec(text);
|
|
229
|
+
view.state.tree.iterate({
|
|
230
|
+
enter(node) {
|
|
231
|
+
const match = /^(?:ATXHeading|SetextHeading)([1-6])$/.exec(node.name);
|
|
232
|
+
if (
|
|
233
|
+
!match ||
|
|
234
|
+
(frontmatter && node.from < frontmatter[0].length) ||
|
|
235
|
+
node.node.parent?.name !== 'Document'
|
|
236
|
+
)
|
|
237
|
+
return;
|
|
238
|
+
const omit = [];
|
|
239
|
+
node.node.toTree().iterate({
|
|
240
|
+
enter(child) {
|
|
241
|
+
if (
|
|
242
|
+
/^(HeaderMark|EmphasisMark|StrikethroughMark|CodeMark|LinkMark|URL|LinkTitle)$/.test(child.name)
|
|
243
|
+
)
|
|
244
|
+
omit.push([node.from + child.from, node.from + child.to]);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
let title = '',
|
|
248
|
+
cursor = node.from;
|
|
249
|
+
for (const [from, to] of omit.sort((a, b) => a[0] - b[0]))
|
|
250
|
+
if (from >= cursor) {
|
|
251
|
+
title += view.state.sliceDoc(cursor, from);
|
|
252
|
+
cursor = to;
|
|
253
|
+
}
|
|
254
|
+
title = (title + view.state.sliceDoc(cursor, node.to)).replace(/\s+/g, ' ').trim();
|
|
255
|
+
if (title) next.push({ id: `heading-${node.from}`, from: node.from, level: Number(match[1]), title });
|
|
256
|
+
return false;
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
if (JSON.stringify(next) !== JSON.stringify(headings)) {
|
|
260
|
+
headings = next;
|
|
261
|
+
render();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function update() {
|
|
265
|
+
frame = null;
|
|
266
|
+
const scroller = view.scrollDOM,
|
|
267
|
+
rect = scroller.getBoundingClientRect(),
|
|
268
|
+
scale = rect.width / scroller.offsetWidth || 1;
|
|
269
|
+
// 以整个 File Panel(含 Toolbar)的中心锚定,换算回编辑器局部坐标。
|
|
270
|
+
if (panel) {
|
|
271
|
+
const panelRect = panel.getBoundingClientRect(),
|
|
272
|
+
editorRect = view.dom.getBoundingClientRect();
|
|
273
|
+
const editorScale = editorRect.height / view.dom.offsetHeight || 1;
|
|
274
|
+
mount.style.setProperty(
|
|
275
|
+
'--mcx-reading-toc-center',
|
|
276
|
+
`${(panelRect.top + panelRect.height / 2 - editorRect.top) / editorScale}px`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const nextVisible =
|
|
280
|
+
headings.length >= 2 && (view.contentDOM.getBoundingClientRect().left - rect.left) / scale >= 112;
|
|
281
|
+
if (!navigating && headings.length) {
|
|
282
|
+
const position = view.lineBlockAtHeight(Math.max(0, rect.top + 24 * scale - view.documentTop)).from;
|
|
283
|
+
active = headings[0].id;
|
|
284
|
+
for (const heading of headings) {
|
|
285
|
+
if (heading.from > position) break;
|
|
286
|
+
active = heading.id;
|
|
287
|
+
}
|
|
288
|
+
if (scroller.scrollTop > 0 && scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 1)
|
|
289
|
+
active = headings.at(-1).id;
|
|
290
|
+
}
|
|
291
|
+
visible = nextVisible;
|
|
292
|
+
render();
|
|
293
|
+
}
|
|
294
|
+
function schedule() {
|
|
295
|
+
if (frame === null) frame = requestAnimationFrame(update);
|
|
296
|
+
}
|
|
297
|
+
view.scrollDOM.addEventListener('scroll', schedule, { signal, passive: true });
|
|
298
|
+
for (const type of ['scrollend', 'wheel', 'touchstart', 'pointerdown', 'keydown'])
|
|
299
|
+
view.scrollDOM.addEventListener(
|
|
300
|
+
type,
|
|
301
|
+
() => {
|
|
302
|
+
navigating = false;
|
|
303
|
+
restoreScroll();
|
|
304
|
+
schedule();
|
|
305
|
+
},
|
|
306
|
+
{ signal, passive: true },
|
|
307
|
+
);
|
|
308
|
+
const resize = new ResizeObserver(schedule);
|
|
309
|
+
resize.observe(view.dom);
|
|
310
|
+
resize.observe(view.contentDOM);
|
|
311
|
+
if (panel) resize.observe(panel);
|
|
312
|
+
const plugin = api.ViewPlugin.fromClass(
|
|
313
|
+
class {
|
|
314
|
+
update(update) {
|
|
315
|
+
if (update.docChanged || update.viewportChanged || update.startState.tree !== update.state.tree) {
|
|
316
|
+
build();
|
|
317
|
+
schedule();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
view.dispatch({ effects: api.StateEffect.appendConfig.of(compartment.of(plugin)) });
|
|
323
|
+
build();
|
|
324
|
+
schedule();
|
|
325
|
+
return () => {
|
|
326
|
+
controller.abort();
|
|
327
|
+
resize.disconnect();
|
|
328
|
+
restoreScroll();
|
|
329
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
330
|
+
root.unmount();
|
|
331
|
+
mount.remove();
|
|
332
|
+
if (!view.destroyed) view.dispatch({ effects: compartment.reconfigure([]) });
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// 输入只有正文与排除区间;编号规则不依赖 Codex 语法树、DOM 或编辑器。
|
|
2
|
+
export function collectMarkdownFootnotes(text, excludedRanges) {
|
|
3
|
+
let offset = 0;
|
|
4
|
+
const lines = text.split('\n').map((value) => {
|
|
5
|
+
const line = { text: value, from: offset, to: offset + value.length };
|
|
6
|
+
offset = line.to + 1;
|
|
7
|
+
return line;
|
|
8
|
+
});
|
|
9
|
+
const excluded = (position) => excludedRanges.some(([from, to]) => position >= from && position < to);
|
|
10
|
+
const definitions = new Map();
|
|
11
|
+
for (let index = 0; index < lines.length; index++) {
|
|
12
|
+
const line = lines[index];
|
|
13
|
+
const match = line.text.match(/^ {0,3}\[\^([^\]]+)\]:\s*(.*)$/);
|
|
14
|
+
if (!match || excluded(line.from)) continue;
|
|
15
|
+
const body = [match[2]];
|
|
16
|
+
let end = index;
|
|
17
|
+
while (end + 1 < lines.length && /^(?: {4}|\t)/.test(lines[end + 1].text)) {
|
|
18
|
+
end++;
|
|
19
|
+
body.push(lines[end].text.replace(/^(?: {4}|\t)/, ''));
|
|
20
|
+
}
|
|
21
|
+
definitions.set(match[1], {
|
|
22
|
+
from: line.from,
|
|
23
|
+
to: lines[end].to,
|
|
24
|
+
body: body.join('\n'),
|
|
25
|
+
refs: [],
|
|
26
|
+
number: 0,
|
|
27
|
+
});
|
|
28
|
+
index = end;
|
|
29
|
+
}
|
|
30
|
+
const notes = [...definitions.values()];
|
|
31
|
+
const references = [];
|
|
32
|
+
let number = 0;
|
|
33
|
+
for (const match of text.matchAll(/\[\^([^\]\n]+)\]/g)) {
|
|
34
|
+
const escaped = (text.slice(0, match.index).match(/\\+$/)?.[0].length || 0) % 2;
|
|
35
|
+
const note = definitions.get(match[1]);
|
|
36
|
+
if (
|
|
37
|
+
escaped ||
|
|
38
|
+
!note ||
|
|
39
|
+
excluded(match.index) ||
|
|
40
|
+
notes.some((note) => match.index >= note.from && match.index < note.to)
|
|
41
|
+
)
|
|
42
|
+
continue;
|
|
43
|
+
if (!note.number) note.number = ++number;
|
|
44
|
+
note.refs.push(match.index);
|
|
45
|
+
references.push({
|
|
46
|
+
id: match[1],
|
|
47
|
+
from: match.index,
|
|
48
|
+
to: match.index + match[0].length,
|
|
49
|
+
number: note.number,
|
|
50
|
+
definitionFrom: note.from,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// 没有引用的定义仍保留,沿用正文末尾的连续编号。
|
|
54
|
+
for (const note of notes) if (!note.number) note.number = ++number;
|
|
55
|
+
return { definitions, references };
|
|
56
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadNativeMarkdownRenderer } from './adapters/codex/markdown-native.mjs';
|
|
2
|
+
import { collectMarkdownFootnotes } from './domain/footnotes.mjs';
|
|
3
|
+
import { captureMarkdownProviders } from './adapters/codex/providers.mjs';
|
|
4
|
+
import {
|
|
5
|
+
createMarkdownReadingRuntime,
|
|
6
|
+
installMarkdownMetadataScroll,
|
|
7
|
+
} from './adapters/codex/reading-runtime.mjs';
|
|
8
|
+
import { loadNativeMarkdownRichText, installMarkdownRichText } from './adapters/codex/rich-text.mjs';
|
|
9
|
+
import {
|
|
10
|
+
loadNativeMarkdownTableActions,
|
|
11
|
+
installMarkdownTableActions,
|
|
12
|
+
} from './adapters/codex/table-actions.mjs';
|
|
13
|
+
import { loadNativeMarkdownLinks, installMarkdownLinks } from './adapters/codex/links.mjs';
|
|
14
|
+
import { loadNativeMarkdownToc } from './adapters/codex/toc-native.mjs';
|
|
15
|
+
import { installMarkdownToc } from './adapters/codex/toc.mjs';
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { installMarkdownTables } from './adapters/codex/tables.mjs';
|
|
18
|
+
import { loadMarkdownListAPI, installMarkdownLists } from './adapters/codex/lists.mjs';
|
|
19
|
+
import { createMarkdownPreviewAdapter } from './adapters/codex/preview.mjs';
|
|
20
|
+
export async function createPageScriptSource() {
|
|
21
|
+
const bindings = [
|
|
22
|
+
collectMarkdownFootnotes,
|
|
23
|
+
captureMarkdownProviders,
|
|
24
|
+
createMarkdownReadingRuntime,
|
|
25
|
+
createMarkdownPreviewAdapter,
|
|
26
|
+
installMarkdownMetadataScroll,
|
|
27
|
+
loadMarkdownListAPI,
|
|
28
|
+
installMarkdownLists,
|
|
29
|
+
loadNativeMarkdownLinks,
|
|
30
|
+
installMarkdownLinks,
|
|
31
|
+
installMarkdownTables,
|
|
32
|
+
loadNativeMarkdownToc,
|
|
33
|
+
installMarkdownToc,
|
|
34
|
+
loadNativeMarkdownRenderer,
|
|
35
|
+
loadNativeMarkdownTableActions,
|
|
36
|
+
installMarkdownTableActions,
|
|
37
|
+
loadNativeMarkdownRichText,
|
|
38
|
+
installMarkdownRichText,
|
|
39
|
+
];
|
|
40
|
+
const declarations = bindings.map((binding) => `const ${binding.name} = ${binding.toString()};`).join('\n');
|
|
41
|
+
const entry = await readFile(new URL('./markdown-reading.mjs', import.meta.url), 'utf8');
|
|
42
|
+
return `${declarations}\n${entry}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const marker = 'data-minecodex-markdown-reading';
|
|
2
|
+
const native = createMarkdownPreviewAdapter(config.codex);
|
|
3
|
+
const styled = new Map();
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
const tokenNames = ['--font-content', '--text-document', '--leading-relaxed', '--font-weight-semibold'];
|
|
6
|
+
const style = document.createElement('style');
|
|
7
|
+
style.setAttribute('data-minecodex-markdown-reading-styles', '');
|
|
8
|
+
style.textContent = config.resources.styles;
|
|
9
|
+
document.head.append(style);
|
|
10
|
+
let disposed = false;
|
|
11
|
+
let pending = null;
|
|
12
|
+
|
|
13
|
+
function release(editor) {
|
|
14
|
+
const binding = styled.get(editor);
|
|
15
|
+
sessions.get(editor)?.dispose();
|
|
16
|
+
sessions.delete(editor);
|
|
17
|
+
editor.removeAttribute(marker);
|
|
18
|
+
binding?.();
|
|
19
|
+
styled.delete(editor);
|
|
20
|
+
}
|
|
21
|
+
function reconcile() {
|
|
22
|
+
pending = null;
|
|
23
|
+
if (disposed) return;
|
|
24
|
+
const previews = native.previews();
|
|
25
|
+
const editors = new Set(previews.map((item) => item.editor));
|
|
26
|
+
for (const editor of styled.keys()) if (!editors.has(editor)) release(editor);
|
|
27
|
+
for (const preview of previews) {
|
|
28
|
+
const { editor } = preview;
|
|
29
|
+
if (!styled.has(editor)) styled.set(editor, config.ui.theme.bindScope(editor, tokenNames));
|
|
30
|
+
if (!editor.hasAttribute(marker)) editor.setAttribute(marker, '');
|
|
31
|
+
if (!sessions.has(editor)) sessions.set(editor, native.attach(preview));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function schedule(records) {
|
|
36
|
+
// 忽略原生选区与逐行装饰更新;只在预览视图或其祖先变化时重新定位。
|
|
37
|
+
if (records.every((record) => record.target.closest?.('.cm-content, [data-mcx-reading-toc]'))) return;
|
|
38
|
+
if (!disposed && pending === null) pending = window.requestAnimationFrame(reconcile);
|
|
39
|
+
}
|
|
40
|
+
const observer = new MutationObserver(schedule);
|
|
41
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
42
|
+
reconcile();
|
|
43
|
+
config.registerSelfCheck?.(() => {
|
|
44
|
+
const previews = native.previews();
|
|
45
|
+
const styleAvailable =
|
|
46
|
+
style.isConnected && previews.length > 0 && previews.every(({ editor }) => editor.hasAttribute(marker));
|
|
47
|
+
const sessionChecks = previews.map(({ editor }) => sessions.get(editor)?.checks() ?? []);
|
|
48
|
+
const checks = previews.length
|
|
49
|
+
? [
|
|
50
|
+
{ name: 'markdown-preview-styled', available: styleAvailable },
|
|
51
|
+
...(sessionChecks[0] ?? []).map((check) => ({
|
|
52
|
+
name: check.name,
|
|
53
|
+
available: sessionChecks.every((items) =>
|
|
54
|
+
items.some((item) => item.name === check.name && item.available),
|
|
55
|
+
),
|
|
56
|
+
})),
|
|
57
|
+
]
|
|
58
|
+
: [];
|
|
59
|
+
const available =
|
|
60
|
+
styleAvailable &&
|
|
61
|
+
sessionChecks.every((items) => items.length > 0) &&
|
|
62
|
+
checks.every((check) => check.available);
|
|
63
|
+
return {
|
|
64
|
+
state: previews.length ? (available ? 'available' : 'unavailable') : 'not-applicable',
|
|
65
|
+
reason: previews.length
|
|
66
|
+
? available
|
|
67
|
+
? null
|
|
68
|
+
: styleAvailable
|
|
69
|
+
? 'markdown-reading-layout-unavailable'
|
|
70
|
+
: 'markdown-style-unavailable'
|
|
71
|
+
: 'no-markdown-preview',
|
|
72
|
+
checks,
|
|
73
|
+
scopes: previews.map(({ editor }) => ({ name: 'markdown-preview', element: editor })),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
disposed = true;
|
|
78
|
+
observer.disconnect();
|
|
79
|
+
if (pending !== null) window.cancelAnimationFrame(pending);
|
|
80
|
+
for (const editor of styled.keys()) release(editor);
|
|
81
|
+
style.remove();
|
|
82
|
+
};
|