codex-overleaf-link 2.1.5 → 2.2.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 (66) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +10 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +14 -5
  9. package/extension/src/content/contentRuntime.js +330 -309
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/panelRenderer.js +112 -12
  17. package/extension/src/content/pendingInputView.js +106 -0
  18. package/extension/src/content/projectProviderSelection.js +68 -0
  19. package/extension/src/content/projectSessionCleanup.js +4 -0
  20. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  21. package/extension/src/content/providerSettingsDialog.js +2 -2
  22. package/extension/src/content/recentProjects.js +7 -2
  23. package/extension/src/content/runGuidanceController.js +70 -0
  24. package/extension/src/content/runGuidanceView.js +48 -0
  25. package/extension/src/content/runQueueScheduler.js +260 -0
  26. package/extension/src/content/runTimelineView.js +33 -80
  27. package/extension/src/content/sessionManager.js +2 -2
  28. package/extension/src/content/sessionPersistence.js +62 -0
  29. package/extension/src/content/settingsPanel.js +1 -1
  30. package/extension/src/content/updateNotice.js +80 -1
  31. package/extension/src/page/writebackRouter.js +3 -9
  32. package/extension/src/shared/compatibility.js +2 -1
  33. package/extension/src/shared/i18n.js +38 -0
  34. package/extension/src/shared/providerProfiles.js +3 -2
  35. package/extension/src/shared/runInputQueue.js +253 -0
  36. package/extension/src/shared/sessionState.js +45 -22
  37. package/extension/src/shared/storageDb.js +39 -44
  38. package/extension/src/shared/storageMigration.js +123 -3
  39. package/extension/src/shared/storageRunActions.js +34 -1
  40. package/extension/src/shared/updateConsent.js +15 -4
  41. package/extension/styles/panel.css +319 -20
  42. package/extension/vendor/katex/katex.min.css +1 -1
  43. package/extension/vendor/markdown-it/LICENSE +22 -0
  44. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  45. package/extension/vendor/markdown-it/VERSION +1 -0
  46. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  47. package/native-host/src/anthropicBridgeRequest.js +3 -3
  48. package/native-host/src/anthropicBridgeResponse.js +58 -17
  49. package/native-host/src/anthropicMessagesBridge.js +156 -23
  50. package/native-host/src/chatBridgeRequest.js +1 -1
  51. package/native-host/src/chatBridgeResponse.js +12 -8
  52. package/native-host/src/chatCompletionsBridge.js +22 -40
  53. package/native-host/src/codexPromptAssembly.js +3 -1
  54. package/native-host/src/codexProviderLaunch.js +6 -3
  55. package/native-host/src/codexSessionRunner.js +65 -65
  56. package/native-host/src/codexSessionTiming.js +45 -0
  57. package/native-host/src/index.js +58 -3
  58. package/native-host/src/managedInstall.js +45 -7
  59. package/native-host/src/providerProfile.js +2 -2
  60. package/native-host/src/providerStreamLifecycle.js +100 -0
  61. package/native-host/src/responsesPassthroughBridge.js +636 -0
  62. package/native-host/src/taskRunnerRuntime.js +81 -2
  63. package/native-host/src/updateArchive.js +4 -3
  64. package/native-host/src/updateManager.js +306 -19
  65. package/native-host/src/updateTrust.js +1 -1
  66. package/package.json +3 -1
@@ -313,6 +313,129 @@
313
313
  }
314
314
  }
315
315
 
316
+ function formatProjectSnapshotUserLog(project, options = {}) {
317
+ const tx = options.tx || ((english) => english);
318
+ const files = project?.files || [];
319
+ if (!files.length) {
320
+ return tx(
321
+ 'No Overleaf project files were read yet. Make sure the project has loaded, or open a .tex file in Overleaf and retry.',
322
+ '还没有读到 Overleaf 项目文件。请确认项目已加载完成,或在 Overleaf 点开一个 .tex 文件后重试。'
323
+ );
324
+ }
325
+ const textCount = files.filter(isTextSnapshotFile).length;
326
+ const binaryCount = files.length - textCount;
327
+ const resourceText = binaryCount ? tx(`, ${binaryCount} asset file(s)`, `,${binaryCount} 个资源文件`) : '';
328
+ const activePath = project.activePath ? tx(`, current file: ${project.activePath}`, `,当前文件:${project.activePath}`) : '';
329
+ const focusFiles = options.getFocusFiles?.() || [];
330
+ const focusText = focusFiles.length ? tx(`, focus: ${focusFiles.join(', ')}`, `,优先处理:${focusFiles.join(', ')}`) : '';
331
+ return tx(
332
+ `Read Overleaf project: ${textCount} text file(s)${resourceText}${activePath}${focusText}.`,
333
+ `已读取 Overleaf 项目:${textCount} 个文本文件${resourceText}${activePath}${focusText}。`
334
+ );
335
+ }
336
+
337
+ function formatProjectSnapshotFileSize(file) {
338
+ if (isTextSnapshotFile(file)) return `${String(file.content || '').length} chars`;
339
+ if (Number.isFinite(Number(file?.size))) return `${Number(file.size)} bytes`;
340
+ return file?.contentBase64 ? `${String(file.contentBase64).length} base64` : 'binary';
341
+ }
342
+
343
+ function formatProjectSnapshotWarning(warning, tx = (english => english)) {
344
+ if (/No source files were captured/i.test(warning)) {
345
+ return tx('No project source files were read. Make sure Overleaf has loaded, or open a .tex file and retry.', '没有读取到项目源文件。请确认 Overleaf 页面加载完成,或点开一个 .tex 文件后重试。');
346
+ }
347
+ if (/Full project snapshot was not captured/i.test(warning)) {
348
+ return tx('The full Overleaf project was not read. To avoid refreshing the local workspace from an incomplete snapshot, reload Overleaf or retry later.', '没有读到完整的 Overleaf 项目。为了避免本地 workspace 用残缺快照覆盖项目,请刷新 Overleaf 或稍后重试。');
349
+ }
350
+ if (/empty or still loading/i.test(warning)) {
351
+ return tx('Some file content is still loading. Wait for Overleaf to finish loading, then retry.', '读取到的文件内容还在加载中。请等 Overleaf 加载完成后重试。');
352
+ }
353
+ if (/suspiciously short|shorter than 80 characters/i.test(warning)) {
354
+ return tx('Some file content is very short and may not have fully loaded. Results may be incomplete.', '部分文件内容很短,可能还没完全加载。结果可能不完整。');
355
+ }
356
+ if (/identical captured content/i.test(warning)) {
357
+ return tx('Several files appear to have identical captured content, so Overleaf file switching may have failed. Reload and retry.', '多个文件内容看起来相同,Overleaf 文件切换可能没有成功。请刷新页面后重试。');
358
+ }
359
+ if (/were skipped/i.test(warning)) {
360
+ return tx('Some project files were skipped, usually images, PDFs, or unreadable files.', '有些项目文件被跳过,通常是图片、PDF 或无法读取的文件。');
361
+ }
362
+ return warning;
363
+ }
364
+
365
+ function appendEditorDiagnostics(editorDiagnostics, projectDiagnostics, appendLog = () => {}) {
366
+ if (editorDiagnostics) {
367
+ const active = editorDiagnostics.active;
368
+ appendLog(`Editor probe: active ${active?.tag || 'none'} ${active?.ariaLabel || active?.className || ''} len=${active?.valueLength || 0}; textareas=${editorDiagnostics.textareaCount || 0}; editables=${editorDiagnostics.editableCount || 0}; iframes=${editorDiagnostics.iframeCount || 0}.`);
369
+ if (editorDiagnostics.documentStats) {
370
+ const stats = editorDiagnostics.documentStats;
371
+ appendLog(`DOM stats: elements=${stats.elementCount || 0}; textareas=${stats.textareaCount || 0}; cm=${stats.cmCount || 0}; role textbox=${stats.roleTextboxCount || 0}.`);
372
+ }
373
+ if (editorDiagnostics.unstableStore) {
374
+ const store = editorDiagnostics.unstableStore;
375
+ const readable = (store.readable || []).map(item => `${item.path}:${item.present ? item.type : 'missing'}`).join(', ');
376
+ appendLog(`Overleaf store: ${store.present ? 'present' : 'missing'}${readable ? `; ${readable}` : ''}.`);
377
+ }
378
+ if (editorDiagnostics.codeMirrorView) {
379
+ const view = editorDiagnostics.codeMirrorView;
380
+ appendLog(`CodeMirror view: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}.`);
381
+ }
382
+ if (editorDiagnostics.globals) {
383
+ const globals = editorDiagnostics.globals;
384
+ appendLog(`Overleaf globals: overleaf=${globals.overleaf || 'missing'}; Overleaf=${globals.Overleaf || 'missing'}; _ide=${globals._ide || 'missing'}.`);
385
+ }
386
+ for (const item of [...(editorDiagnostics.textareas || []), ...(editorDiagnostics.editables || [])].slice(0, 3)) {
387
+ appendLog(`Editor candidate: ${item.tag || 'node'} ${item.ariaLabel || item.className || item.id || ''} len=${item.valueLength || 0}.`);
388
+ }
389
+ }
390
+ if (projectDiagnostics) {
391
+ const records = projectDiagnostics.docRecords || [];
392
+ appendLog(`Project probe: doc records=${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}.`);
393
+ for (const record of records.slice(0, 4)) appendLog(`Doc record: ${record.path} id=${record.id} source=${record.source || 'internal'}.`);
394
+ }
395
+ }
396
+
397
+ function appendProjectWarnings(project, options = {}) {
398
+ const warnings = options.getWarnings?.(project) || { blocking: [], nonBlocking: [] };
399
+ for (const warning of warnings.blocking) options.appendLog?.(`Snapshot blocked: ${warning}`);
400
+ for (const warning of warnings.nonBlocking) options.appendLog?.(`Snapshot warning: ${warning}`);
401
+ }
402
+
403
+ function isTextSnapshotFile(file) {
404
+ return Boolean(file && file.kind !== 'binary' && typeof file.content === 'string');
405
+ }
406
+
407
+ function uniqueContentSignatures(files) {
408
+ return Array.from(new Set(files.map(file => {
409
+ const content = String(file.content || '');
410
+ return `${content.length}:${content.slice(0, 120)}:${content.slice(-120)}`;
411
+ })));
412
+ }
413
+
414
+ function appendReviewingDiagnostics(diagnostics, options = {}) {
415
+ const appendLog = options.appendLog || (() => {});
416
+ const tx = options.tx || ((english) => english);
417
+ if (!diagnostics) {
418
+ appendLog(tx('Probe diagnostics unavailable.', '诊断探针不可用。'));
419
+ return;
420
+ }
421
+ appendLog(`Probe saw ${diagnostics.controlCount || 0} controls; body Reviewing=${Boolean(diagnostics.bodyTextHasReviewing)}, text Reviewing=${Boolean(diagnostics.textContentHasReviewing)}.`);
422
+ const controls = diagnostics.reviewLikeControls || [];
423
+ if (!controls.length) {
424
+ appendLog('Probe did not see review/track/suggest controls in the page DOM.');
425
+ return;
426
+ }
427
+ for (const control of controls.slice(0, 4)) {
428
+ const label = [
429
+ control.text,
430
+ control.ariaLabel && `aria:${control.ariaLabel}`,
431
+ control.title && `title:${control.title}`,
432
+ control.dataTestId && `test:${control.dataTestId}`,
433
+ control.id && `id:${control.id}`
434
+ ].filter(Boolean).join(' | ');
435
+ appendLog(`Review-like ${control.tag || 'node'}: ${label || control.htmlSnippet || '(no label)'}`);
436
+ }
437
+ }
438
+
316
439
  function t(instance, key, params) {
317
440
  if (typeof instance?.i18n === 'function') {
318
441
  return instance.i18n(key, params);
@@ -331,12 +454,20 @@
331
454
  }
332
455
 
333
456
  window.CodexOverleafDiagnosticsPanel = {
457
+ appendEditorDiagnostics,
458
+ appendProjectWarnings,
459
+ appendReviewingDiagnostics,
334
460
  create,
461
+ formatProjectSnapshotFileSize,
462
+ formatProjectSnapshotUserLog,
463
+ formatProjectSnapshotWarning,
464
+ isTextSnapshotFile,
335
465
  updateStatus,
336
466
  toggleMenu,
337
467
  closeMenu,
338
468
  closeResult,
339
469
  showLoading,
340
- showResult
470
+ showResult,
471
+ uniqueContentSignatures
341
472
  };
342
473
  })();
@@ -0,0 +1,451 @@
1
+ (function initCodexOverleafMarkdownDomRenderer(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) {
4
+ module.exports = api;
5
+ } else {
6
+ root.CodexOverleafMarkdownDomRenderer = api;
7
+ }
8
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function markdownDomRendererFactory() {
9
+ 'use strict';
10
+
11
+ const DEFAULT_LIMITS = Object.freeze({
12
+ maxChars: 1_000_000,
13
+ maxTokens: 25_000,
14
+ maxNesting: 32
15
+ });
16
+ const BLOCK_TAGS = new Set([
17
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
18
+ 'blockquote', 'ul', 'ol', 'li',
19
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td'
20
+ ]);
21
+ const INLINE_TAGS = new Map([
22
+ ['em_open', 'em'],
23
+ ['strong_open', 'strong'],
24
+ ['s_open', 's']
25
+ ]);
26
+ const probedFontDocuments = new WeakSet();
27
+ let cachedMarkdownIt = null;
28
+ let cachedMathText = null;
29
+ let cachedMaxNesting = null;
30
+ let cachedParser = null;
31
+
32
+ function renderMarkdown(target, value, options = {}) {
33
+ if (!target || typeof target.replaceChildren !== 'function') {
34
+ throw rendererError('markdown_target_invalid', 'Markdown target is unavailable.');
35
+ }
36
+ const document = options.document || target.ownerDocument;
37
+ if (!document || typeof document.createDocumentFragment !== 'function') {
38
+ throw rendererError('markdown_document_invalid', 'Markdown document is unavailable.');
39
+ }
40
+ const source = stripEmptyHtmlComments(String(value || ''));
41
+ const limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
42
+ if (source.length > limits.maxChars) {
43
+ throw rendererError('markdown_source_too_large', `Markdown source exceeds ${limits.maxChars} UTF-16 code units.`);
44
+ }
45
+
46
+ const parser = options.parser || getParser({
47
+ markdownIt: options.markdownIt,
48
+ mathText: options.mathText,
49
+ maxNesting: limits.maxNesting
50
+ });
51
+ const tokens = parser.parse(source, {});
52
+ const tokenCount = countTokens(tokens);
53
+ if (tokenCount > limits.maxTokens) {
54
+ throw rendererError('markdown_token_limit', `Markdown token count exceeds ${limits.maxTokens}.`);
55
+ }
56
+
57
+ const fragment = document.createDocumentFragment();
58
+ const context = {
59
+ document,
60
+ renderTextNodes: options.renderTextNodes || (text => [document.createTextNode(text)]),
61
+ renderLinkNodes: options.renderLinkNodes || (label => [document.createTextNode(label)]),
62
+ renderCodeNodes: options.renderCodeNodes,
63
+ sanitizeText: options.sanitizeText || (text => String(text || '')),
64
+ createMathNode: options.createMathNode,
65
+ recordDiagnostic: options.recordDiagnostic,
66
+ mathRendered: false
67
+ };
68
+ renderBlockTokens(tokens, fragment, context);
69
+ target.replaceChildren(fragment);
70
+ if (context.mathRendered) {
71
+ probeMathFonts(document, context.recordDiagnostic);
72
+ }
73
+ return { tokenCount };
74
+ }
75
+
76
+ function getParser({ markdownIt, mathText, maxNesting }) {
77
+ if (typeof markdownIt !== 'function') {
78
+ throw rendererError('markdown_parser_missing', 'The vendored Markdown parser is unavailable.');
79
+ }
80
+ if (!mathText || typeof mathText.matchMathAt !== 'function') {
81
+ throw rendererError('markdown_math_rule_missing', 'The shared math delimiter matcher is unavailable.');
82
+ }
83
+ if (
84
+ cachedParser &&
85
+ cachedMarkdownIt === markdownIt &&
86
+ cachedMathText === mathText &&
87
+ cachedMaxNesting === maxNesting
88
+ ) {
89
+ return cachedParser;
90
+ }
91
+ const parser = markdownIt({
92
+ html: false,
93
+ linkify: false,
94
+ typographer: false,
95
+ breaks: false,
96
+ maxNesting
97
+ });
98
+ installMathRules(parser, mathText);
99
+ cachedMarkdownIt = markdownIt;
100
+ cachedMathText = mathText;
101
+ cachedMaxNesting = maxNesting;
102
+ cachedParser = parser;
103
+ return parser;
104
+ }
105
+
106
+ function installMathRules(parser, mathText) {
107
+ parser.inline.ruler.before('escape', 'codex_math_inline', (state, silent) => {
108
+ const match = mathText.matchMathAt(state.src, state.pos);
109
+ if (!match) {
110
+ return false;
111
+ }
112
+ if (!silent) {
113
+ const token = state.push('math_inline', 'span', 0);
114
+ token.content = match.value;
115
+ token.markup = match.raw.slice(0, 2);
116
+ token.meta = { display: match.display, raw: match.raw };
117
+ }
118
+ state.pos = match.end;
119
+ return true;
120
+ });
121
+
122
+ parser.block.ruler.before('lheading', 'codex_math_block', (state, startLine, endLine, silent) => {
123
+ const start = state.bMarks[startLine] + state.tShift[startLine];
124
+ const match = mathText.matchMathAt(state.src, start);
125
+ if (!match || !match.display) {
126
+ return false;
127
+ }
128
+ let lastLine = startLine;
129
+ while (lastLine < endLine && state.eMarks[lastLine] < match.end) {
130
+ lastLine += 1;
131
+ }
132
+ if (lastLine >= endLine || state.src.slice(match.end, state.eMarks[lastLine]).trim()) {
133
+ return false;
134
+ }
135
+ if (silent) {
136
+ return true;
137
+ }
138
+ const token = state.push('math_block', 'div', 0);
139
+ token.block = true;
140
+ token.content = match.value;
141
+ token.map = [startLine, lastLine + 1];
142
+ token.markup = match.raw.slice(0, 2);
143
+ token.meta = { display: true, raw: match.raw };
144
+ state.line = lastLine + 1;
145
+ return true;
146
+ }, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
147
+ }
148
+
149
+ function renderBlockTokens(tokens, rootNode, context) {
150
+ const stack = [{ node: rootNode, closeType: '' }];
151
+ for (let index = 0; index < tokens.length; index += 1) {
152
+ const token = tokens[index];
153
+ const parent = stack[stack.length - 1].node;
154
+
155
+ if (
156
+ token.type === 'paragraph_open' &&
157
+ tokens[index + 1]?.type === 'inline' &&
158
+ tokens[index + 2]?.type === 'paragraph_close'
159
+ ) {
160
+ const children = tokens[index + 1].children || [];
161
+ if (token.hidden) {
162
+ renderInlineTokens(children, parent, context);
163
+ } else {
164
+ renderParagraphFlow(children, parent, context);
165
+ }
166
+ index += 2;
167
+ continue;
168
+ }
169
+
170
+ if (token.type === 'inline') {
171
+ renderInlineTokens(token.children || [], parent, context);
172
+ continue;
173
+ }
174
+ if (token.type === 'fence' || token.type === 'code_block') {
175
+ appendCodeBlock(parent, token, context);
176
+ continue;
177
+ }
178
+ if (token.type === 'math_block') {
179
+ parent.append(createMathNode(token, context, true));
180
+ continue;
181
+ }
182
+ if (token.type === 'hr') {
183
+ parent.append(context.document.createElement('hr'));
184
+ continue;
185
+ }
186
+ if (token.nesting === 1) {
187
+ const node = createBlockContainer(token, context);
188
+ parent.append(node);
189
+ stack.push({
190
+ node,
191
+ closeType: token.type.replace(/_open$/, '_close')
192
+ });
193
+ continue;
194
+ }
195
+ if (token.nesting === -1) {
196
+ const frame = stack.pop();
197
+ if (!frame || frame.closeType !== token.type || stack.length === 0) {
198
+ throw rendererError('markdown_structure_invalid', `Unexpected closing token: ${token.type}.`);
199
+ }
200
+ continue;
201
+ }
202
+ throw rendererError('markdown_token_unsupported', `Unsupported Markdown token: ${token.type}.`);
203
+ }
204
+ if (stack.length !== 1) {
205
+ throw rendererError('markdown_structure_invalid', 'Markdown token nesting did not close cleanly.');
206
+ }
207
+ }
208
+
209
+ function createBlockContainer(token, context) {
210
+ const tag = String(token.tag || '').toLowerCase();
211
+ if (!BLOCK_TAGS.has(tag)) {
212
+ throw rendererError('markdown_block_unsupported', `Unsupported Markdown container: ${tag || token.type}.`);
213
+ }
214
+ const node = context.document.createElement(tag);
215
+ if (tag === 'table') {
216
+ node.className = 'run-markdown-table';
217
+ }
218
+ if (tag === 'blockquote') {
219
+ node.className = 'run-markdown-quote';
220
+ }
221
+ if (tag === 'ol') {
222
+ const start = Number(token.attrGet?.('start'));
223
+ if (Number.isSafeInteger(start) && start > 1) {
224
+ node.setAttribute('start', String(start));
225
+ }
226
+ }
227
+ if (tag === 'th' || tag === 'td') {
228
+ const alignment = String(token.attrGet?.('style') || '').match(/text-align:\s*(left|center|right)/i)?.[1];
229
+ if (alignment) {
230
+ node.classList.add(`is-align-${alignment.toLowerCase()}`);
231
+ }
232
+ }
233
+ return node;
234
+ }
235
+
236
+ function appendCodeBlock(parent, token, context) {
237
+ const pre = context.document.createElement('pre');
238
+ pre.className = 'run-code-block';
239
+ const code = context.document.createElement('code');
240
+ code.textContent = context.sanitizeText(token.content || '');
241
+ const language = String(token.info || '').trim().split(/\s+/)[0].replace(/[^a-z0-9_-]/gi, '');
242
+ if (language) {
243
+ code.className = `language-${language}`;
244
+ }
245
+ pre.append(code);
246
+ parent.append(pre);
247
+ }
248
+
249
+ function renderParagraphFlow(tokens, parent, context) {
250
+ let segment = [];
251
+ const flush = () => {
252
+ if (!segment.length) {
253
+ return;
254
+ }
255
+ const paragraph = context.document.createElement('p');
256
+ const meaningful = segment.filter(token => token.type !== 'softbreak' && (
257
+ token.type !== 'text' || String(token.content || '').trim()
258
+ ));
259
+ renderInlineTokens(segment, paragraph, context, {
260
+ standaloneCode: meaningful.length === 1 && meaningful[0].type === 'code_inline'
261
+ });
262
+ if (paragraphHasContent(paragraph)) {
263
+ parent.append(paragraph);
264
+ }
265
+ segment = [];
266
+ };
267
+ let nesting = 0;
268
+ for (const token of tokens) {
269
+ if (token.nesting === 1) nesting += 1;
270
+ if (token.type === 'math_inline' && token.meta?.display && nesting === 0) {
271
+ flush();
272
+ parent.append(createMathNode(token, context, true));
273
+ continue;
274
+ }
275
+ segment.push(token);
276
+ if (token.nesting === -1) nesting = Math.max(0, nesting - 1);
277
+ }
278
+ flush();
279
+ }
280
+
281
+ function paragraphHasContent(node) {
282
+ return Boolean(node.childNodes?.length || String(node.textContent || '').trim());
283
+ }
284
+
285
+ function renderInlineTokens(tokens, rootNode, context, renderOptions = {}) {
286
+ const stack = [{ node: rootNode, closeType: '' }];
287
+ for (let index = 0; index < tokens.length; index += 1) {
288
+ const token = tokens[index];
289
+ const parent = stack[stack.length - 1].node;
290
+ if (token.type === 'text' || token.type === 'html_inline') {
291
+ appendNodes(parent, context.renderTextNodes(context.sanitizeText(token.content || '')));
292
+ continue;
293
+ }
294
+ if (token.type === 'softbreak') {
295
+ parent.append(context.document.createTextNode(' '));
296
+ continue;
297
+ }
298
+ if (token.type === 'hardbreak') {
299
+ parent.append(context.document.createElement('br'));
300
+ continue;
301
+ }
302
+ if (token.type === 'code_inline') {
303
+ if (renderOptions.standaloneCode && typeof context.renderCodeNodes === 'function') {
304
+ appendNodes(parent, context.renderCodeNodes(context.sanitizeText(token.content || '')));
305
+ continue;
306
+ }
307
+ const code = context.document.createElement('code');
308
+ code.className = 'run-inline-code';
309
+ code.textContent = context.sanitizeText(token.content || '');
310
+ parent.append(code);
311
+ continue;
312
+ }
313
+ if (token.type === 'math_inline') {
314
+ parent.append(createMathNode(token, context, Boolean(token.meta?.display)));
315
+ continue;
316
+ }
317
+ if (token.type === 'image') {
318
+ appendNodes(parent, context.renderTextNodes(context.sanitizeText(token.content || token.attrGet?.('alt') || '')));
319
+ continue;
320
+ }
321
+ if (token.type === 'link_open') {
322
+ const closeIndex = findMatchingInlineClose(tokens, index, 'link_open', 'link_close');
323
+ const label = extractInlineText(tokens.slice(index + 1, closeIndex));
324
+ const href = String(token.attrGet?.('href') || '');
325
+ appendNodes(parent, context.renderLinkNodes(context.sanitizeText(label), href));
326
+ index = closeIndex;
327
+ continue;
328
+ }
329
+ if (INLINE_TAGS.has(token.type)) {
330
+ const node = context.document.createElement(INLINE_TAGS.get(token.type));
331
+ parent.append(node);
332
+ stack.push({ node, closeType: token.type.replace(/_open$/, '_close') });
333
+ continue;
334
+ }
335
+ if (token.nesting === -1) {
336
+ const frame = stack.pop();
337
+ if (!frame || frame.closeType !== token.type || stack.length === 0) {
338
+ throw rendererError('markdown_inline_structure_invalid', `Unexpected inline closing token: ${token.type}.`);
339
+ }
340
+ continue;
341
+ }
342
+ throw rendererError('markdown_inline_unsupported', `Unsupported inline Markdown token: ${token.type}.`);
343
+ }
344
+ if (stack.length !== 1) {
345
+ throw rendererError('markdown_inline_structure_invalid', 'Inline Markdown token nesting did not close cleanly.');
346
+ }
347
+ }
348
+
349
+ function findMatchingInlineClose(tokens, start, openType, closeType) {
350
+ let depth = 0;
351
+ for (let index = start; index < tokens.length; index += 1) {
352
+ if (tokens[index].type === openType) depth += 1;
353
+ if (tokens[index].type === closeType) depth -= 1;
354
+ if (depth === 0) return index;
355
+ }
356
+ throw rendererError('markdown_link_unclosed', 'Markdown link did not close cleanly.');
357
+ }
358
+
359
+ function extractInlineText(tokens) {
360
+ return tokens.map(token => {
361
+ if (token.type === 'image') return token.content || token.attrGet?.('alt') || '';
362
+ if (token.children) return extractInlineText(token.children);
363
+ return token.content || '';
364
+ }).join('');
365
+ }
366
+
367
+ function createMathNode(token, context, forceDisplay) {
368
+ if (typeof context.createMathNode !== 'function') {
369
+ throw rendererError('markdown_math_renderer_missing', 'The KaTeX node renderer is unavailable.');
370
+ }
371
+ const display = Boolean(forceDisplay || token.meta?.display);
372
+ const raw = token.meta?.raw || (display ? `$$${token.content}$$` : `$${token.content}$`);
373
+ const node = context.createMathNode({
374
+ type: 'math',
375
+ raw,
376
+ value: token.content,
377
+ display
378
+ });
379
+ if (!node) {
380
+ throw rendererError('markdown_math_node_invalid', 'The KaTeX node renderer returned no node.');
381
+ }
382
+ context.mathRendered = true;
383
+ return node;
384
+ }
385
+
386
+ function appendNodes(parent, value) {
387
+ const nodes = Array.isArray(value) ? value : (value ? [value] : []);
388
+ for (const node of nodes) {
389
+ if (node) parent.append(node);
390
+ }
391
+ }
392
+
393
+ function countTokens(tokens) {
394
+ let total = 0;
395
+ const visit = list => {
396
+ for (const token of list || []) {
397
+ total += 1;
398
+ if (token.children?.length) visit(token.children);
399
+ }
400
+ };
401
+ visit(tokens);
402
+ return total;
403
+ }
404
+
405
+ function stripEmptyHtmlComments(value) {
406
+ return String(value || '').replace(/<!--\s*-->/g, '');
407
+ }
408
+
409
+ function probeMathFonts(document, recordDiagnostic) {
410
+ const fonts = document.fonts;
411
+ if (!fonts || typeof fonts.load !== 'function' || probedFontDocuments.has(document)) {
412
+ return;
413
+ }
414
+ probedFontDocuments.add(document);
415
+ const families = ['KaTeX_Main', 'KaTeX_Math', 'KaTeX_AMS', 'KaTeX_Size1'];
416
+ Promise.allSettled(families.map(family => fonts.load(`16px "${family}"`)))
417
+ .then(async results => {
418
+ await Promise.resolve(fonts.ready);
419
+ const missing = results.flatMap((result, index) => (
420
+ result.status === 'rejected' || !result.value?.length ? [families[index]] : []
421
+ ));
422
+ if (missing.length && typeof recordDiagnostic === 'function') {
423
+ recordDiagnostic({
424
+ code: 'katex_font_load_incomplete',
425
+ detail: `KaTeX font families did not load: ${missing.join(', ')}`
426
+ });
427
+ }
428
+ })
429
+ .catch(() => {
430
+ if (typeof recordDiagnostic === 'function') {
431
+ recordDiagnostic({
432
+ code: 'katex_font_probe_failed',
433
+ detail: 'KaTeX font readiness could not be verified.'
434
+ });
435
+ }
436
+ });
437
+ }
438
+
439
+ function rendererError(code, message) {
440
+ const error = new Error(message);
441
+ error.code = code;
442
+ return error;
443
+ }
444
+
445
+ return {
446
+ DEFAULT_LIMITS,
447
+ countTokens,
448
+ renderMarkdown,
449
+ stripEmptyHtmlComments
450
+ };
451
+ });