omni-viewer-core 0.11.0 → 0.12.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.
- package/dist/i18n/catalog.en.js +0 -2
- package/dist/i18n/catalog.ja.js +0 -2
- package/dist/i18n/catalog.ko.js +0 -2
- package/dist/i18n/catalog.zh-cn.js +0 -2
- package/dist/parsers/latex/index.d.ts +5 -1
- package/dist/parsers/latex/index.js +22 -8
- package/dist/styles/latex.css +1 -1
- package/dist/viewers/csv/controller.d.ts +6 -1
- package/dist/viewers/csv/index.js +18 -6
- package/dist/viewers/diagram-controller.d.ts +7 -1
- package/dist/viewers/diagram-controller.js +2 -1
- package/dist/viewers/diagram.d.ts +6 -1
- package/dist/viewers/diagram.js +15 -4
- package/dist/viewers/latex/controller.d.ts +10 -1
- package/dist/viewers/latex/controller.js +2 -1
- package/dist/viewers/latex/index.d.ts +7 -1
- package/dist/viewers/latex/index.js +16 -7
- package/dist/viewers/latex/styles.d.ts +1 -1
- package/dist/viewers/latex/styles.js +1 -1
- package/dist/viewers/markdown/controller.d.ts +6 -1
- package/dist/viewers/markdown/controller.js +2 -1
- package/dist/viewers/markdown/index.d.ts +6 -1
- package/dist/viewers/markdown/index.js +18 -5
- package/dist/viewers/mermaid/index.d.ts +4 -3
- package/dist/viewers/pdf/controller.d.ts +11 -1
- package/dist/viewers/pdf/controller.js +8 -1
- package/dist/viewers/pdf/index.js +8 -1
- package/dist/viewers/plantuml/index.d.ts +4 -3
- package/package.json +1 -1
package/dist/i18n/catalog.en.js
CHANGED
|
@@ -136,7 +136,6 @@ export const CATALOG_EN = {
|
|
|
136
136
|
'latex.documentClass': 'Class: {name}', 'latex.sectionCount': '{count} section(s)',
|
|
137
137
|
'latex.unsupportedEnvironment': '{environment} — not rendered',
|
|
138
138
|
'latex.unresolvedInput': '\\{command} — external file not resolved',
|
|
139
|
-
'latex.floatCaptionOnly': '{environment} — caption only',
|
|
140
139
|
'latex.parseFailed': 'This file could not be read as a LaTeX document.',
|
|
141
140
|
'latex.mathNotInstalled': 'Math renderer not installed — formulas are shown as TeX',
|
|
142
141
|
'latex.mathFailed': 'This formula could not be rendered; its TeX source is shown.',
|
|
@@ -159,7 +158,6 @@ export const CATALOG_EN = {
|
|
|
159
158
|
'diag.latex.limit-exceeded': 'The LaTeX document exceeds a supported limit.',
|
|
160
159
|
'diag.latex.depth-exceeded': 'The LaTeX document nests deeper than the supported limit.',
|
|
161
160
|
'diag.latex.math-limit-exceeded': 'A formula exceeds the supported size and is shown as source.',
|
|
162
|
-
'diag.latex.float-caption-only': 'Only the caption of the {environment} environment is shown.',
|
|
163
161
|
'archive.title': 'Archive', 'archive.search': 'Search entries', 'archive.entries': '{count} entries',
|
|
164
162
|
'archive.previewUnavailable': 'Preview unavailable', 'archive.mediaUnsupported': 'Media preview is unsupported.',
|
|
165
163
|
'archive.encrypted': 'This archive is encrypted.', 'archive.encryptedEntries': 'This archive contains {count} encrypted entries.',
|
package/dist/i18n/catalog.ja.js
CHANGED
|
@@ -140,7 +140,6 @@ export const CATALOG_JA = {
|
|
|
140
140
|
'latex.documentClass': 'クラス: {name}', 'latex.sectionCount': 'セクション {count} 個',
|
|
141
141
|
'latex.unsupportedEnvironment': '{environment} — レンダーしません',
|
|
142
142
|
'latex.unresolvedInput': '\\{command} — 外部ファイルは解決しません',
|
|
143
|
-
'latex.floatCaptionOnly': '{environment} — キャプションのみ',
|
|
144
143
|
'latex.parseFailed': 'このファイルはLaTeX文書として読み取れませんでした。',
|
|
145
144
|
'latex.mathNotInstalled': '数式レンダラーが未インストールのため、数式をTeXソースで表示します',
|
|
146
145
|
'latex.mathFailed': 'この数式をレンダーできなかったため、TeXソースを表示します。',
|
|
@@ -163,7 +162,6 @@ export const CATALOG_JA = {
|
|
|
163
162
|
'diag.latex.limit-exceeded': 'LaTeX文書が対応上限を超えています。',
|
|
164
163
|
'diag.latex.depth-exceeded': 'LaTeX文書の入れ子が対応上限より深くなっています。',
|
|
165
164
|
'diag.latex.math-limit-exceeded': '数式が対応サイズを超えたため、ソースとして表示します。',
|
|
166
|
-
'diag.latex.float-caption-only': '{environment}環境はキャプションのみ表示します。',
|
|
167
165
|
'archive.title': 'アーカイブ', 'archive.search': '項目を検索', 'archive.entries': '{count}項目',
|
|
168
166
|
'archive.previewUnavailable': 'プレビューできません', 'archive.mediaUnsupported': 'メディアのプレビューには対応していません。',
|
|
169
167
|
'archive.encrypted': 'このアーカイブは暗号化されています。', 'archive.encryptedEntries': 'このアーカイブには暗号化された項目が{count}件あります。',
|
package/dist/i18n/catalog.ko.js
CHANGED
|
@@ -136,7 +136,6 @@ export const CATALOG_KO = {
|
|
|
136
136
|
'latex.documentClass': '클래스: {name}', 'latex.sectionCount': '절 {count}개',
|
|
137
137
|
'latex.unsupportedEnvironment': '{environment} — 렌더하지 않음',
|
|
138
138
|
'latex.unresolvedInput': '\\{command} — 외부 파일을 해석하지 않음',
|
|
139
|
-
'latex.floatCaptionOnly': '{environment} — 캡션만 표시',
|
|
140
139
|
'latex.parseFailed': 'LaTeX 문서로 읽을 수 없는 파일입니다.',
|
|
141
140
|
'latex.mathNotInstalled': '수식 렌더러가 설치되지 않아 수식을 TeX 원문으로 표시합니다',
|
|
142
141
|
'latex.mathFailed': '이 수식을 렌더하지 못해 TeX 원문을 표시합니다.',
|
|
@@ -159,7 +158,6 @@ export const CATALOG_KO = {
|
|
|
159
158
|
'diag.latex.limit-exceeded': 'LaTeX 문서가 지원 한도를 초과합니다.',
|
|
160
159
|
'diag.latex.depth-exceeded': 'LaTeX 문서의 중첩이 지원 한도보다 깊습니다.',
|
|
161
160
|
'diag.latex.math-limit-exceeded': '수식이 지원 크기를 초과하여 원문으로 표시합니다.',
|
|
162
|
-
'diag.latex.float-caption-only': '{environment} 환경은 캡션만 표시합니다.',
|
|
163
161
|
'archive.title': '압축 파일', 'archive.search': '항목 검색', 'archive.entries': '항목 {count}개',
|
|
164
162
|
'archive.previewUnavailable': '미리보기를 사용할 수 없음', 'archive.mediaUnsupported': '미디어 미리보기는 지원되지 않습니다.',
|
|
165
163
|
'archive.encrypted': '이 압축 파일은 암호화되어 있습니다.', 'archive.encryptedEntries': '이 압축 파일에는 암호화된 항목이 {count}개 있습니다.',
|
|
@@ -133,7 +133,6 @@ export const CATALOG_ZH_CN = {
|
|
|
133
133
|
'latex.documentClass': '文档类:{name}', 'latex.sectionCount': '{count} 个章节',
|
|
134
134
|
'latex.unsupportedEnvironment': '{environment} — 不渲染',
|
|
135
135
|
'latex.unresolvedInput': '\\{command} — 不解析外部文件',
|
|
136
|
-
'latex.floatCaptionOnly': '{environment} — 仅显示题注',
|
|
137
136
|
'latex.parseFailed': '无法将此文件作为 LaTeX 文档读取。',
|
|
138
137
|
'latex.mathNotInstalled': '未安装公式渲染器 — 公式以 TeX 源代码显示',
|
|
139
138
|
'latex.mathFailed': '无法渲染此公式,改为显示其 TeX 源代码。',
|
|
@@ -156,7 +155,6 @@ export const CATALOG_ZH_CN = {
|
|
|
156
155
|
'diag.latex.limit-exceeded': 'LaTeX 文档超过支持的限制。',
|
|
157
156
|
'diag.latex.depth-exceeded': 'LaTeX 文档的嵌套超过支持的深度限制。',
|
|
158
157
|
'diag.latex.math-limit-exceeded': '公式超过支持的大小,改为显示源代码。',
|
|
159
|
-
'diag.latex.float-caption-only': '{environment} 环境仅显示题注。',
|
|
160
158
|
'archive.title': '压缩包', 'archive.search': '搜索条目', 'archive.entries': '{count} 个条目',
|
|
161
159
|
'archive.previewUnavailable': '无法预览', 'archive.mediaUnsupported': '不支持媒体预览。',
|
|
162
160
|
'archive.encrypted': '此压缩包已加密。', 'archive.encryptedEntries': '此压缩包包含 {count} 个加密条目。',
|
|
@@ -107,10 +107,14 @@ export type LatexBlock = {
|
|
|
107
107
|
kind: 'verbatim';
|
|
108
108
|
source: string;
|
|
109
109
|
span: LatexSpan;
|
|
110
|
-
}
|
|
110
|
+
}
|
|
111
|
+
/** A `figure`/`table` float. Its body is scanned like any other content —
|
|
112
|
+
* the caption is lifted out for display, not used as a replacement for it. */
|
|
113
|
+
| {
|
|
111
114
|
kind: 'float';
|
|
112
115
|
environment: string;
|
|
113
116
|
caption?: LatexInline[];
|
|
117
|
+
blocks: LatexBlock[];
|
|
114
118
|
span: LatexSpan;
|
|
115
119
|
} | {
|
|
116
120
|
kind: 'unresolved';
|
|
@@ -629,15 +629,16 @@ function readEnvironment(ctx, name, start, contentStart, end, depth) {
|
|
|
629
629
|
};
|
|
630
630
|
}
|
|
631
631
|
if (FLOAT_ENVIRONMENTS.has(name)) {
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
632
|
+
// The caption is lifted out for display, but the body is scanned like
|
|
633
|
+
// any other content (L22). Extracting only the caption — the original
|
|
634
|
+
// L7 reading, from before tables rendered — discarded the `tabular` that
|
|
635
|
+
// is the whole point of a `table` float, and not even as source.
|
|
635
636
|
const caption = readCaption(ctx, inner, depth);
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
637
|
+
const blocks = scanBlocks(ctx, contentStart, closing.contentEnd, depth + 1);
|
|
638
|
+
return {
|
|
639
|
+
blocks: made(ctx, { kind: 'float', environment: name, ...(caption ? { caption } : {}), blocks, span }),
|
|
640
|
+
next: closing.after
|
|
641
|
+
};
|
|
641
642
|
}
|
|
642
643
|
addDiagnostic(ctx, 'warning', 'latex.unsupported-environment', 'diag.latex.unsupported-environment', { environment: name });
|
|
643
644
|
return { blocks: made(ctx, { kind: 'unsupported', environment: name, source: text.slice(start, closing.after), span }), next: closing.after };
|
|
@@ -1213,6 +1214,19 @@ function readInlineCommand(ctx, source, control, depth, out, flush) {
|
|
|
1213
1214
|
const group = readGroup(source, control.end, source.length);
|
|
1214
1215
|
return group ? group.end : control.end;
|
|
1215
1216
|
}
|
|
1217
|
+
// The float's caption is lifted out separately, so the copy in the body is
|
|
1218
|
+
// consumed here. `\caption*` and the optional short form both occur, which
|
|
1219
|
+
// is why this is not a plain entry in SILENT_COMMANDS.
|
|
1220
|
+
if (control.value === 'caption') {
|
|
1221
|
+
let cursor = control.end;
|
|
1222
|
+
if (source[cursor] === '*')
|
|
1223
|
+
cursor++;
|
|
1224
|
+
const optional = readOptional(source, cursor, source.length);
|
|
1225
|
+
if (optional)
|
|
1226
|
+
cursor = optional.end;
|
|
1227
|
+
const group = readGroup(source, skipSpace(source, cursor), source.length);
|
|
1228
|
+
return group ? group.end : cursor;
|
|
1229
|
+
}
|
|
1216
1230
|
// `\let\abs=\envert` — the `=` is optional, so a fixed argument count would
|
|
1217
1231
|
// leave part of it behind as prose.
|
|
1218
1232
|
if (control.value === 'let') {
|
package/dist/styles/latex.css
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
.omni-latex__theorem{margin:0 0 1em;padding:12px 14px;border-left:3px solid var(--tex-border);background:rgba(127,127,127,.06);border-radius:0 6px 6px 0}.omni-latex__theorem-title{margin-bottom:.5em;font-weight:600}.omni-latex__theorem-note{margin-left:6px;font-weight:400;color:var(--tex-muted)}.omni-latex__theorem-body{font-style:italic}.omni-latex__theorem-body p:last-child{margin-bottom:0}
|
|
39
39
|
.omni-latex__verbatim{margin:0 0 .9em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px;background:rgba(127,127,127,.08);font:12px/1.5 var(--omni-mono,monospace);white-space:pre;overflow-x:auto}
|
|
40
40
|
.omni-latex__unsupported{margin:0 0 .9em;border:1px dashed var(--tex-border);border-radius:6px;background:rgba(127,127,127,.06);overflow:hidden}.omni-latex__badge{display:block;padding:6px 12px;border-bottom:1px dashed var(--tex-border);color:var(--omni-warning,#ffd98a);font-size:11px;font-weight:600;letter-spacing:.02em}.omni-latex__unsupported pre{margin:0;padding:12px 14px;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto;color:var(--tex-muted)}.omni-latex__unsupported--inline{display:inline;border:0;background:none;padding:0 2px;color:var(--tex-muted);font:12px/1.5 var(--omni-mono,monospace)}
|
|
41
|
-
.omni-latex__float{margin:0 0
|
|
41
|
+
.omni-latex__float{margin:0 0 1em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px}.omni-latex__float-body{display:flex;flex-direction:column;align-items:center}.omni-latex__float-body>*{max-width:100%}.omni-latex__float-caption{margin-top:.6em;color:var(--tex-muted);font-size:.94em;text-align:center}
|
|
42
42
|
.omni-latex__source{flex:1;width:100%;min-height:62vh;resize:none;border:0;outline:0;padding:16px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e));color:inherit;font:13px/1.55 var(--omni-mono,monospace);tab-size:4}
|
|
43
43
|
.omni-latex__warning{padding:10px 12px;border:1px solid #b98b2f;border-radius:6px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);white-space:pre-wrap}.omni-latex__warning[hidden]{display:none}.omni-latex__message{padding:10px 12px;border:1px solid var(--omni-error,#f44747);border-radius:6px;color:var(--omni-error,#f44747);white-space:pre-wrap}.omni-latex__message[hidden]{display:none}
|
|
44
44
|
@media(max-width:900px){.omni-latex__workspace,.omni-latex__workspace.is-split{grid-template-columns:1fr}}
|
|
@@ -115,7 +115,12 @@ export interface CsvController {
|
|
|
115
115
|
/** Whole document in original order as normalized CSV (save/writeback —
|
|
116
116
|
* ignores search and sort so hidden rows are never dropped). */
|
|
117
117
|
toDocumentCsv(): string;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Mark the current edit depth as the saved point (clears dirty). Writes are
|
|
120
|
+
* async and the grid stays editable during one, so the caller must confirm
|
|
121
|
+
* that `toDocumentCsv()` still equals what it wrote before calling this —
|
|
122
|
+
* otherwise an edit made mid-write is recorded as being on disk.
|
|
123
|
+
*/
|
|
119
124
|
markSaved(): void;
|
|
120
125
|
/** Decoded source text (raw view). */
|
|
121
126
|
readonly rawText: string;
|
|
@@ -216,13 +216,25 @@ export async function mountCsvViewer(input, container, ctx, options = {}) {
|
|
|
216
216
|
saveAsBtn.title = t('common.noFileSave');
|
|
217
217
|
}
|
|
218
218
|
const dirtyBadge = el('span', 'omni-csv__dirty');
|
|
219
|
+
/**
|
|
220
|
+
* Clear dirty only if the document still equals the bytes that reached the
|
|
221
|
+
* file. The grid stays editable across the async write, so a cell changed
|
|
222
|
+
* mid-write would otherwise be recorded as saved — and a host trusting
|
|
223
|
+
* `isDirty()` would discard it on re-mount. When it differs, dirty is left
|
|
224
|
+
* set: the next save resolves it, and erring toward "unsaved" cannot lose
|
|
225
|
+
* work.
|
|
226
|
+
*/
|
|
227
|
+
function markSavedIfUnchanged(written) {
|
|
228
|
+
if (controller.toDocumentCsv() === written)
|
|
229
|
+
controller.markSaved();
|
|
230
|
+
}
|
|
219
231
|
async function saveDocument(service) {
|
|
220
232
|
try {
|
|
221
233
|
// Save covers the whole document in original order — never the
|
|
222
234
|
// filtered/sorted display result (docs §6).
|
|
223
|
-
const
|
|
224
|
-
await service.write(
|
|
225
|
-
|
|
235
|
+
const written = controller.toDocumentCsv();
|
|
236
|
+
await service.write(new TextEncoder().encode(written));
|
|
237
|
+
markSavedIfUnchanged(written);
|
|
226
238
|
showToast(t('common.savedToOriginal'));
|
|
227
239
|
}
|
|
228
240
|
catch (error) {
|
|
@@ -237,9 +249,9 @@ export async function mountCsvViewer(input, container, ctx, options = {}) {
|
|
|
237
249
|
const name = saveAsFileName(input.fileName, state.delimiter);
|
|
238
250
|
const mime = state.delimiter === '\t' ? 'text/tab-separated-values' : 'text/csv';
|
|
239
251
|
try {
|
|
240
|
-
const
|
|
241
|
-
await service.saveFile(name,
|
|
242
|
-
|
|
252
|
+
const written = controller.toDocumentCsv();
|
|
253
|
+
await service.saveFile(name, new TextEncoder().encode(written), mime);
|
|
254
|
+
markSavedIfUnchanged(written);
|
|
243
255
|
showToast(t('common.saved', { name }));
|
|
244
256
|
}
|
|
245
257
|
catch (error) {
|
|
@@ -19,8 +19,14 @@ export type DiagramAction = {
|
|
|
19
19
|
type: 'undo';
|
|
20
20
|
} | {
|
|
21
21
|
type: 'redo';
|
|
22
|
-
}
|
|
22
|
+
}
|
|
23
|
+
/** `source` is the text that actually reached the file. Writes are async and
|
|
24
|
+
* the editor stays live during one, so the current source at completion is
|
|
25
|
+
* not necessarily what was written (mirrors the latex/markdown controllers).
|
|
26
|
+
* Omitted = "what is in the editor now". */
|
|
27
|
+
| {
|
|
23
28
|
type: 'mark-saved';
|
|
29
|
+
source?: string;
|
|
24
30
|
};
|
|
25
31
|
export interface DiagramViewState {
|
|
26
32
|
mode: DiagramViewMode;
|
|
@@ -38,7 +38,8 @@ export function createDiagramController(text, theme) {
|
|
|
38
38
|
setSource(redo.pop());
|
|
39
39
|
}
|
|
40
40
|
else if (action.type === 'mark-saved') {
|
|
41
|
-
|
|
41
|
+
const savedSource = action.source ?? state.source;
|
|
42
|
+
state = { ...state, savedSource, dirty: state.source !== savedSource };
|
|
42
43
|
}
|
|
43
44
|
emit();
|
|
44
45
|
},
|
|
@@ -8,6 +8,11 @@ export type DiagramViewerContext = HostContext & {
|
|
|
8
8
|
writeback?: FileWritebackService;
|
|
9
9
|
save?: FileSaveService;
|
|
10
10
|
};
|
|
11
|
+
/** Diagram mount handle: `isDirty()` lets a host guard a re-mount against
|
|
12
|
+
* discarding unsaved source edits (DESIGN.md §3-②). */
|
|
13
|
+
export interface DiagramViewerHandle extends ViewerHandle {
|
|
14
|
+
isDirty(): boolean;
|
|
15
|
+
}
|
|
11
16
|
export interface DiagramMountOptions extends MountOptions {
|
|
12
17
|
renderMermaid?(id: string, source: string, theme?: DiagramTheme): Promise<string>;
|
|
13
18
|
renderPlantUml?(source: string, document: Document, theme?: DiagramTheme): SVGElement | string;
|
|
@@ -20,5 +25,5 @@ export interface DiagramMountOptions extends MountOptions {
|
|
|
20
25
|
export declare function mountDiagramViewer(kind: DiagramKind, parsed: {
|
|
21
26
|
source: string;
|
|
22
27
|
warnings: string[];
|
|
23
|
-
}, input: ViewerInput, container: HTMLElement, ctx: DiagramViewerContext, options?: DiagramMountOptions): Promise<
|
|
28
|
+
}, input: ViewerInput, container: HTMLElement, ctx: DiagramViewerContext, options?: DiagramMountOptions): Promise<DiagramViewerHandle>;
|
|
24
29
|
export declare function sanitizeSvg(svg: string, maxBytes?: number, maxNodes?: number): SVGElement;
|
package/dist/viewers/diagram.js
CHANGED
|
@@ -181,13 +181,20 @@ export async function mountDiagramViewer(kind, parsed, input, container, ctx, op
|
|
|
181
181
|
}
|
|
182
182
|
};
|
|
183
183
|
const saveSource = async () => {
|
|
184
|
-
|
|
184
|
+
// Snapshot once: the write is async and the editor stays live, so
|
|
185
|
+
// `controller.state.source` after the await may already be a later edit
|
|
186
|
+
// than the bytes that reached the file.
|
|
187
|
+
const saved = controller.state.source;
|
|
188
|
+
const bytes = new TextEncoder().encode(saved);
|
|
185
189
|
if (ctx.writeback) {
|
|
190
|
+
// The caption is the dirty indicator: leave syncState's verdict alone
|
|
191
|
+
// when an edit arrived mid-write and the source is still unsaved.
|
|
186
192
|
try {
|
|
187
193
|
await ctx.writeback.write(bytes);
|
|
188
|
-
controller.dispatch({ type: 'mark-saved' });
|
|
194
|
+
controller.dispatch({ type: 'mark-saved', source: saved });
|
|
189
195
|
setStatus('common.savedToOriginal', 'valid');
|
|
190
|
-
|
|
196
|
+
if (!controller.state.dirty)
|
|
197
|
+
sourceCaption.textContent = t('common.savedToOriginal');
|
|
191
198
|
}
|
|
192
199
|
catch (error) {
|
|
193
200
|
ctx.logger.log('error', `diagram save failed: ${String(error)}`);
|
|
@@ -296,7 +303,11 @@ export async function mountDiagramViewer(kind, parsed, input, container, ctx, op
|
|
|
296
303
|
container.classList.remove(VIEWER_ROOT_CLASS, `omni-viewer--${kind}`);
|
|
297
304
|
else
|
|
298
305
|
root.replaceChildren();
|
|
299
|
-
}
|
|
306
|
+
},
|
|
307
|
+
// The controller, not the textarea: `mark-saved` (a successful writeback)
|
|
308
|
+
// is the only thing that clears it, so a failed save or a Save As copy
|
|
309
|
+
// stays dirty.
|
|
310
|
+
isDirty: () => controller.state.dirty
|
|
300
311
|
};
|
|
301
312
|
}
|
|
302
313
|
let nextId = 1;
|
|
@@ -12,8 +12,17 @@ export type LatexAction = {
|
|
|
12
12
|
type: 'undo';
|
|
13
13
|
} | {
|
|
14
14
|
type: 'redo';
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `source` is the text that actually reached the file. Writes are async and
|
|
18
|
+
* the editor stays live during one, so the current source at completion is
|
|
19
|
+
* not necessarily what was written — omitting it would mark an edit made
|
|
20
|
+
* mid-write as saved and let a host discard it. Omitted = "what is in the
|
|
21
|
+
* editor now", which is only correct for synchronous saves.
|
|
22
|
+
*/
|
|
23
|
+
| {
|
|
16
24
|
type: 'mark-saved';
|
|
25
|
+
source?: string;
|
|
17
26
|
};
|
|
18
27
|
export interface LatexViewState {
|
|
19
28
|
mode: LatexViewMode;
|
|
@@ -52,7 +52,8 @@ export function createLatexController(text, mode = 'preview') {
|
|
|
52
52
|
setSource(redo.pop());
|
|
53
53
|
}
|
|
54
54
|
else if (action.type === 'mark-saved') {
|
|
55
|
-
|
|
55
|
+
const savedSource = action.source ?? state.source;
|
|
56
|
+
state = { ...state, savedSource, dirty: state.source !== savedSource };
|
|
56
57
|
}
|
|
57
58
|
emit();
|
|
58
59
|
},
|
|
@@ -28,6 +28,12 @@ export interface LatexViewerDeps {
|
|
|
28
28
|
math?: MathRenderer;
|
|
29
29
|
createDOMPurify?(window: Window): DomPurify;
|
|
30
30
|
}
|
|
31
|
+
/** LaTeX mount handle: dirty inspection lets hosts guard a re-mount (file
|
|
32
|
+
* change, refresh) against discarding unsaved edits, without reaching into the
|
|
33
|
+
* viewer's DOM (csv/image/pdf expose the same method). */
|
|
34
|
+
export interface LatexViewerHandle extends ViewerHandle {
|
|
35
|
+
isDirty(): boolean;
|
|
36
|
+
}
|
|
31
37
|
export interface LatexMountOptions extends MountOptions {
|
|
32
38
|
/** Forwarded to the parser (limits, cancellation). */
|
|
33
39
|
parse?: LatexParseOptions;
|
|
@@ -57,7 +63,7 @@ export declare const LATEX_VIEWER_META: {
|
|
|
57
63
|
optionalServices: readonly ["clipboard", "navigation", "writeback", "save"];
|
|
58
64
|
inputOwnership: "borrows";
|
|
59
65
|
};
|
|
60
|
-
export declare function mountLatexViewer(input: ViewerInput, container: HTMLElement, ctx: LatexViewerContext, options?: LatexMountOptions): Promise<
|
|
66
|
+
export declare function mountLatexViewer(input: ViewerInput, container: HTMLElement, ctx: LatexViewerContext, options?: LatexMountOptions): Promise<LatexViewerHandle>;
|
|
61
67
|
/**
|
|
62
68
|
* Commands that produce no glyphs but that math engines do not implement.
|
|
63
69
|
* Collecting the document's own macros (L18) means a text-mode macro can now
|
|
@@ -387,10 +387,12 @@ export async function mountLatexViewer(input, container, ctx, options = {}) {
|
|
|
387
387
|
continue;
|
|
388
388
|
}
|
|
389
389
|
if (block.kind === 'float') {
|
|
390
|
-
const float = el('
|
|
391
|
-
|
|
390
|
+
const float = el('figure', 'omni-latex__float');
|
|
391
|
+
const body = el('div', 'omni-latex__float-body');
|
|
392
|
+
renderBlocks(block.blocks, body);
|
|
393
|
+
float.append(body);
|
|
392
394
|
if (block.caption) {
|
|
393
|
-
const caption = el('
|
|
395
|
+
const caption = el('figcaption', 'omni-latex__float-caption');
|
|
394
396
|
renderInline(block.caption, caption);
|
|
395
397
|
float.append(caption);
|
|
396
398
|
}
|
|
@@ -541,11 +543,15 @@ export async function mountLatexViewer(input, container, ctx, options = {}) {
|
|
|
541
543
|
}
|
|
542
544
|
};
|
|
543
545
|
const saveSource = async () => {
|
|
544
|
-
|
|
546
|
+
// Snapshot once: the write is async and the editor stays live, so
|
|
547
|
+
// `controller.state.source` after the await may already be a later edit
|
|
548
|
+
// than the bytes that reached the file.
|
|
549
|
+
const saved = controller.state.source;
|
|
550
|
+
const bytes = new TextEncoder().encode(saved);
|
|
545
551
|
if (ctx.writeback) {
|
|
546
552
|
try {
|
|
547
553
|
await ctx.writeback.write(bytes);
|
|
548
|
-
controller.dispatch({ type: 'mark-saved' });
|
|
554
|
+
controller.dispatch({ type: 'mark-saved', source: saved });
|
|
549
555
|
setStatus('common.savedToOriginal', 'valid');
|
|
550
556
|
}
|
|
551
557
|
catch (error) {
|
|
@@ -639,7 +645,10 @@ export async function mountLatexViewer(input, container, ctx, options = {}) {
|
|
|
639
645
|
teardown();
|
|
640
646
|
throw new MountAbortedError();
|
|
641
647
|
}
|
|
642
|
-
|
|
648
|
+
// Reads the controller rather than the textarea: the source of truth for
|
|
649
|
+
// "unsaved" is `source !== savedSource`, which undo restores and `mark-saved`
|
|
650
|
+
// clears. A Save As copy never dispatches `mark-saved`, so it stays dirty.
|
|
651
|
+
return { dispose: teardown, isDirty: () => controller.state.dirty };
|
|
643
652
|
}
|
|
644
653
|
function errorText(error, fallback) {
|
|
645
654
|
return error instanceof Error ? error.message : fallback;
|
|
@@ -682,7 +691,7 @@ function hasMath(blocks) {
|
|
|
682
691
|
|| (block.kind === 'theorem' && hasMath(block.body))
|
|
683
692
|
|| (block.kind === 'include' && hasMath(block.blocks))
|
|
684
693
|
|| (block.kind === 'table' && block.rows.some(row => row.some(cell => cell.content.some(node => node.kind === 'math'))))
|
|
685
|
-
|| (block.kind === 'float' && (block.caption ?? []).some(node => node.kind === 'math')));
|
|
694
|
+
|| (block.kind === 'float' && (hasMath(block.blocks) || (block.caption ?? []).some(node => node.kind === 'math'))));
|
|
686
695
|
}
|
|
687
696
|
/** Heading ids are core-generated (`heading-3`), so escaping only needs to
|
|
688
697
|
* cover the leading-digit case CSS selectors reject. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const latexViewerCss = "\n.omni-viewer--latex{box-sizing:border-box;min-height:100%;padding:18px;display:flex;flex-direction:column;gap:14px;color:var(--omni-fg,#d4d4d4);background:var(--omni-bg,#1e1e1e);font:var(--omni-font-size,13px)/1.5 var(--omni-font-family,system-ui,sans-serif);--tex-border:var(--omni-border,#454545);--tex-muted:var(--omni-muted,#aaa);--tex-panel:var(--omni-panel-bg,rgba(127,127,127,.05))}\n.omni-latex *{box-sizing:border-box}.omni-latex__header,.omni-latex__toolbar,.omni-latex__toolbar-group,.omni-latex__panel-header{display:flex;align-items:center;gap:10px}.omni-latex__header{align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.omni-latex__title{font-size:16px;font-weight:600;margin-bottom:4px;word-break:break-word}.omni-latex__meta,.omni-latex__caption{color:var(--tex-muted);font-size:12px}\n.omni-latex__status{padding:7px 10px;border-radius:999px;border:1px solid var(--tex-border);background:rgba(79,193,255,.16);font-size:12px;font-weight:600;white-space:nowrap}.omni-latex__status.is-valid{background:rgba(46,160,67,.16);color:var(--omni-success,#3fb950)}.omni-latex__status.is-invalid{background:rgba(244,71,71,.14);color:var(--omni-error,#f44747)}\n.omni-latex__partial{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid #b98b2f;border-radius:999px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);font-size:12px;font-weight:600}\n.omni-latex__toolbar{flex-wrap:wrap}.omni-latex__toolbar-group{flex-wrap:wrap;padding:7px;border:1px solid var(--tex-border);border-radius:8px;background:var(--tex-panel)}.omni-latex__button{cursor:pointer;border:1px solid var(--tex-border);background:var(--omni-button-secondary-bg,rgba(127,127,127,.16));color:inherit;border-radius:6px;padding:7px 10px;font:inherit}.omni-latex__button:hover:not(:disabled){background:rgba(127,127,127,.26)}.omni-latex__button.is-active{border-color:var(--omni-focus,#007fd4);background:color-mix(in srgb,var(--omni-focus,#007fd4) 22%,transparent)}.omni-latex__button.is-dirty{border-color:var(--omni-warning,#cca700)}.omni-latex__button:disabled{cursor:default;opacity:.5}\n/* Reading views are outline + one pane; split drops the outline and gives the\n preview and the editor an equal half each. */\n.omni-latex__workspace{display:grid;grid-template-columns:minmax(200px,240px) minmax(320px,1fr);gap:14px;min-height:68vh}.omni-latex__workspace.is-split{grid-template-columns:minmax(280px,1fr) minmax(280px,1fr)}.omni-latex__panel{display:flex;flex-direction:column;min-height:0;border:1px solid var(--tex-border);border-radius:8px;background:var(--tex-panel);overflow:hidden}.omni-latex__panel[hidden]{display:none}.omni-latex__panel-header{justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--tex-border)}\n.omni-latex__outline{flex:1;min-height:0;overflow:auto;padding:8px;margin:0;list-style:none}.omni-latex__outline-item{margin:0}.omni-latex__outline-link{display:block;width:100%;text-align:left;cursor:pointer;border:0;background:transparent;color:inherit;font:inherit;padding:5px 8px;border-radius:5px;word-break:break-word}.omni-latex__outline-link:hover{background:rgba(127,127,127,.18)}.omni-latex__outline-link.is-active{background:color-mix(in srgb,var(--omni-focus,#007fd4) 24%,transparent)}.omni-latex__outline-link[data-level=\"2\"]{padding-left:20px}.omni-latex__outline-link[data-level=\"3\"]{padding-left:32px}.omni-latex__outline-link[data-level=\"4\"]{padding-left:44px}.omni-latex__outline-link[data-level=\"5\"]{padding-left:56px}.omni-latex__outline-link[data-level=\"6\"]{padding-left:68px}.omni-latex__outline-empty{padding:10px;color:var(--tex-muted)}\n.omni-latex__preview{flex:1;min-height:0;overflow:auto;padding:22px 26px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e))}.omni-latex__preview h1,.omni-latex__preview h2,.omni-latex__preview h3,.omni-latex__preview h4,.omni-latex__preview h5,.omni-latex__preview h6{margin:1.4em 0 .5em;line-height:1.3;scroll-margin-top:12px}.omni-latex__preview h1{font-size:1.7em}.omni-latex__preview h2{font-size:1.45em}.omni-latex__preview h3{font-size:1.25em}.omni-latex__preview p{margin:0 0 .9em}.omni-latex__preview ul,.omni-latex__preview ol{margin:0 0 .9em;padding-left:1.6em}.omni-latex__preview li{margin:.25em 0}\n.omni-latex__math{display:block;margin:1em 0;padding:10px 12px;border-left:3px solid var(--omni-focus,#007fd4);background:rgba(127,127,127,.08);font:13px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto}.omni-latex__math--inline{display:inline;margin:0;padding:1px 5px;border-left:0;border-radius:4px}\n/* Once rendered the slot is KaTeX's own layout, so the source-code framing is\n dropped. Two things matter here beyond looks:\n - overflow is 'auto hidden', never 'auto'. A formula that is a pixel too wide\n raises a horizontal scrollbar, which eats content-box height, which raises a\n vertical scrollbar, which narrows the box, which keeps the horizontal one \u2014\n a loop that puts both bars on a formula with room to spare.\n - line-height returns to normal. KaTeX sizes its own struts, and inheriting\n the body's 1.5 makes the rendered box taller than the line it sits on. */\n.omni-latex__math.is-rendered{padding:.4em .2em;border-left:0;background:none;font:inherit;line-height:normal;white-space:normal;text-align:center;overflow:auto hidden}\n.omni-latex__math--inline.is-rendered{padding:0;text-align:inherit;overflow:visible}.omni-latex__math.is-pending{opacity:.75}\n/* A formula the engine refused is TeX source, not broken output \u2014 it keeps the\n source framing (mono, left aligned, muted) with a red rule to mark the\n failure. Colouring the whole block red reads as \"this document is wrong\". */\n.omni-latex__math.is-invalid{border-left:3px solid var(--omni-error,#f44747);color:var(--tex-muted);text-align:left;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;padding:10px 12px;background:rgba(244,71,71,.06)}\n.omni-latex__math--inline.is-invalid{border-left:0;padding:1px 5px;text-decoration:underline wavy var(--omni-error,#f44747)}\n/* Belt and braces for renderers that do not honour an errorColor option: KaTeX\n writes its error colour as an inline style, so overriding needs !important. */\n.omni-latex__math .katex-error{color:var(--omni-warning,#cca700)!important;text-decoration:underline dotted}\n.omni-latex__math .katex-display{margin:0;padding:0}.omni-latex__math .katex-display>.katex{max-width:100%}.omni-latex__math .katex{font-size:1.05em}\n.omni-latex__cite{padding:0 3px;border-radius:4px;background:rgba(127,127,127,.18);color:var(--tex-muted);font-size:.92em}\n.omni-latex__ref{padding:0 3px;border:0;border-radius:4px;background:rgba(127,127,127,.18);color:var(--tex-muted);font:inherit;font-size:.92em;cursor:pointer}.omni-latex__ref:hover{background:rgba(127,127,127,.3);color:var(--omni-fg,#d4d4d4)}\n.omni-latex__preview .is-target{outline:2px solid var(--omni-focus,#007fd4);outline-offset:3px;border-radius:4px;transition:outline-color .3s}\n.omni-latex__link{color:var(--omni-link,#4fc1ff);cursor:pointer;background:none;border:0;padding:0;font:inherit;text-decoration:underline}.omni-latex__link:disabled{cursor:default;text-decoration:none;color:inherit}\n.omni-latex__include{margin:0 0 1em;padding-left:12px;border-left:2px dashed var(--tex-border)}.omni-latex__include-label{margin-bottom:.6em;color:var(--tex-muted);font-size:11px;font-weight:600;letter-spacing:.02em}\n.omni-latex__table-wrap{margin:0 0 1em;overflow-x:auto}.omni-latex__table{border-collapse:collapse;min-width:min(100%,340px);font-size:.96em}.omni-latex__table th,.omni-latex__table td{border:1px solid var(--tex-border);padding:6px 10px;vertical-align:top}.omni-latex__table th{background:rgba(127,127,127,.12);font-weight:600;text-align:inherit}\n.omni-latex__theorem{margin:0 0 1em;padding:12px 14px;border-left:3px solid var(--tex-border);background:rgba(127,127,127,.06);border-radius:0 6px 6px 0}.omni-latex__theorem-title{margin-bottom:.5em;font-weight:600}.omni-latex__theorem-note{margin-left:6px;font-weight:400;color:var(--tex-muted)}.omni-latex__theorem-body{font-style:italic}.omni-latex__theorem-body p:last-child{margin-bottom:0}\n.omni-latex__verbatim{margin:0 0 .9em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px;background:rgba(127,127,127,.08);font:12px/1.5 var(--omni-mono,monospace);white-space:pre;overflow-x:auto}\n.omni-latex__unsupported{margin:0 0 .9em;border:1px dashed var(--tex-border);border-radius:6px;background:rgba(127,127,127,.06);overflow:hidden}.omni-latex__badge{display:block;padding:6px 12px;border-bottom:1px dashed var(--tex-border);color:var(--omni-warning,#ffd98a);font-size:11px;font-weight:600;letter-spacing:.02em}.omni-latex__unsupported pre{margin:0;padding:12px 14px;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto;color:var(--tex-muted)}.omni-latex__unsupported--inline{display:inline;border:0;background:none;padding:0 2px;color:var(--tex-muted);font:12px/1.5 var(--omni-mono,monospace)}\n.omni-latex__float{margin:0 0 .9em;padding:12px 14px;border:1px dashed var(--tex-border);border-radius:6px;text-align:center;color:var(--tex-muted)}.omni-latex__float-caption{color:var(--omni-fg,#d4d4d4)}\n.omni-latex__source{flex:1;width:100%;min-height:62vh;resize:none;border:0;outline:0;padding:16px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e));color:inherit;font:13px/1.55 var(--omni-mono,monospace);tab-size:4}\n.omni-latex__warning{padding:10px 12px;border:1px solid #b98b2f;border-radius:6px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);white-space:pre-wrap}.omni-latex__warning[hidden]{display:none}.omni-latex__message{padding:10px 12px;border:1px solid var(--omni-error,#f44747);border-radius:6px;color:var(--omni-error,#f44747);white-space:pre-wrap}.omni-latex__message[hidden]{display:none}\n@media(max-width:900px){.omni-latex__workspace,.omni-latex__workspace.is-split{grid-template-columns:1fr}}\n";
|
|
1
|
+
export declare const latexViewerCss = "\n.omni-viewer--latex{box-sizing:border-box;min-height:100%;padding:18px;display:flex;flex-direction:column;gap:14px;color:var(--omni-fg,#d4d4d4);background:var(--omni-bg,#1e1e1e);font:var(--omni-font-size,13px)/1.5 var(--omni-font-family,system-ui,sans-serif);--tex-border:var(--omni-border,#454545);--tex-muted:var(--omni-muted,#aaa);--tex-panel:var(--omni-panel-bg,rgba(127,127,127,.05))}\n.omni-latex *{box-sizing:border-box}.omni-latex__header,.omni-latex__toolbar,.omni-latex__toolbar-group,.omni-latex__panel-header{display:flex;align-items:center;gap:10px}.omni-latex__header{align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.omni-latex__title{font-size:16px;font-weight:600;margin-bottom:4px;word-break:break-word}.omni-latex__meta,.omni-latex__caption{color:var(--tex-muted);font-size:12px}\n.omni-latex__status{padding:7px 10px;border-radius:999px;border:1px solid var(--tex-border);background:rgba(79,193,255,.16);font-size:12px;font-weight:600;white-space:nowrap}.omni-latex__status.is-valid{background:rgba(46,160,67,.16);color:var(--omni-success,#3fb950)}.omni-latex__status.is-invalid{background:rgba(244,71,71,.14);color:var(--omni-error,#f44747)}\n.omni-latex__partial{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid #b98b2f;border-radius:999px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);font-size:12px;font-weight:600}\n.omni-latex__toolbar{flex-wrap:wrap}.omni-latex__toolbar-group{flex-wrap:wrap;padding:7px;border:1px solid var(--tex-border);border-radius:8px;background:var(--tex-panel)}.omni-latex__button{cursor:pointer;border:1px solid var(--tex-border);background:var(--omni-button-secondary-bg,rgba(127,127,127,.16));color:inherit;border-radius:6px;padding:7px 10px;font:inherit}.omni-latex__button:hover:not(:disabled){background:rgba(127,127,127,.26)}.omni-latex__button.is-active{border-color:var(--omni-focus,#007fd4);background:color-mix(in srgb,var(--omni-focus,#007fd4) 22%,transparent)}.omni-latex__button.is-dirty{border-color:var(--omni-warning,#cca700)}.omni-latex__button:disabled{cursor:default;opacity:.5}\n/* Reading views are outline + one pane; split drops the outline and gives the\n preview and the editor an equal half each. */\n.omni-latex__workspace{display:grid;grid-template-columns:minmax(200px,240px) minmax(320px,1fr);gap:14px;min-height:68vh}.omni-latex__workspace.is-split{grid-template-columns:minmax(280px,1fr) minmax(280px,1fr)}.omni-latex__panel{display:flex;flex-direction:column;min-height:0;border:1px solid var(--tex-border);border-radius:8px;background:var(--tex-panel);overflow:hidden}.omni-latex__panel[hidden]{display:none}.omni-latex__panel-header{justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--tex-border)}\n.omni-latex__outline{flex:1;min-height:0;overflow:auto;padding:8px;margin:0;list-style:none}.omni-latex__outline-item{margin:0}.omni-latex__outline-link{display:block;width:100%;text-align:left;cursor:pointer;border:0;background:transparent;color:inherit;font:inherit;padding:5px 8px;border-radius:5px;word-break:break-word}.omni-latex__outline-link:hover{background:rgba(127,127,127,.18)}.omni-latex__outline-link.is-active{background:color-mix(in srgb,var(--omni-focus,#007fd4) 24%,transparent)}.omni-latex__outline-link[data-level=\"2\"]{padding-left:20px}.omni-latex__outline-link[data-level=\"3\"]{padding-left:32px}.omni-latex__outline-link[data-level=\"4\"]{padding-left:44px}.omni-latex__outline-link[data-level=\"5\"]{padding-left:56px}.omni-latex__outline-link[data-level=\"6\"]{padding-left:68px}.omni-latex__outline-empty{padding:10px;color:var(--tex-muted)}\n.omni-latex__preview{flex:1;min-height:0;overflow:auto;padding:22px 26px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e))}.omni-latex__preview h1,.omni-latex__preview h2,.omni-latex__preview h3,.omni-latex__preview h4,.omni-latex__preview h5,.omni-latex__preview h6{margin:1.4em 0 .5em;line-height:1.3;scroll-margin-top:12px}.omni-latex__preview h1{font-size:1.7em}.omni-latex__preview h2{font-size:1.45em}.omni-latex__preview h3{font-size:1.25em}.omni-latex__preview p{margin:0 0 .9em}.omni-latex__preview ul,.omni-latex__preview ol{margin:0 0 .9em;padding-left:1.6em}.omni-latex__preview li{margin:.25em 0}\n.omni-latex__math{display:block;margin:1em 0;padding:10px 12px;border-left:3px solid var(--omni-focus,#007fd4);background:rgba(127,127,127,.08);font:13px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto}.omni-latex__math--inline{display:inline;margin:0;padding:1px 5px;border-left:0;border-radius:4px}\n/* Once rendered the slot is KaTeX's own layout, so the source-code framing is\n dropped. Two things matter here beyond looks:\n - overflow is 'auto hidden', never 'auto'. A formula that is a pixel too wide\n raises a horizontal scrollbar, which eats content-box height, which raises a\n vertical scrollbar, which narrows the box, which keeps the horizontal one \u2014\n a loop that puts both bars on a formula with room to spare.\n - line-height returns to normal. KaTeX sizes its own struts, and inheriting\n the body's 1.5 makes the rendered box taller than the line it sits on. */\n.omni-latex__math.is-rendered{padding:.4em .2em;border-left:0;background:none;font:inherit;line-height:normal;white-space:normal;text-align:center;overflow:auto hidden}\n.omni-latex__math--inline.is-rendered{padding:0;text-align:inherit;overflow:visible}.omni-latex__math.is-pending{opacity:.75}\n/* A formula the engine refused is TeX source, not broken output \u2014 it keeps the\n source framing (mono, left aligned, muted) with a red rule to mark the\n failure. Colouring the whole block red reads as \"this document is wrong\". */\n.omni-latex__math.is-invalid{border-left:3px solid var(--omni-error,#f44747);color:var(--tex-muted);text-align:left;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;padding:10px 12px;background:rgba(244,71,71,.06)}\n.omni-latex__math--inline.is-invalid{border-left:0;padding:1px 5px;text-decoration:underline wavy var(--omni-error,#f44747)}\n/* Belt and braces for renderers that do not honour an errorColor option: KaTeX\n writes its error colour as an inline style, so overriding needs !important. */\n.omni-latex__math .katex-error{color:var(--omni-warning,#cca700)!important;text-decoration:underline dotted}\n.omni-latex__math .katex-display{margin:0;padding:0}.omni-latex__math .katex-display>.katex{max-width:100%}.omni-latex__math .katex{font-size:1.05em}\n.omni-latex__cite{padding:0 3px;border-radius:4px;background:rgba(127,127,127,.18);color:var(--tex-muted);font-size:.92em}\n.omni-latex__ref{padding:0 3px;border:0;border-radius:4px;background:rgba(127,127,127,.18);color:var(--tex-muted);font:inherit;font-size:.92em;cursor:pointer}.omni-latex__ref:hover{background:rgba(127,127,127,.3);color:var(--omni-fg,#d4d4d4)}\n.omni-latex__preview .is-target{outline:2px solid var(--omni-focus,#007fd4);outline-offset:3px;border-radius:4px;transition:outline-color .3s}\n.omni-latex__link{color:var(--omni-link,#4fc1ff);cursor:pointer;background:none;border:0;padding:0;font:inherit;text-decoration:underline}.omni-latex__link:disabled{cursor:default;text-decoration:none;color:inherit}\n.omni-latex__include{margin:0 0 1em;padding-left:12px;border-left:2px dashed var(--tex-border)}.omni-latex__include-label{margin-bottom:.6em;color:var(--tex-muted);font-size:11px;font-weight:600;letter-spacing:.02em}\n.omni-latex__table-wrap{margin:0 0 1em;overflow-x:auto}.omni-latex__table{border-collapse:collapse;min-width:min(100%,340px);font-size:.96em}.omni-latex__table th,.omni-latex__table td{border:1px solid var(--tex-border);padding:6px 10px;vertical-align:top}.omni-latex__table th{background:rgba(127,127,127,.12);font-weight:600;text-align:inherit}\n.omni-latex__theorem{margin:0 0 1em;padding:12px 14px;border-left:3px solid var(--tex-border);background:rgba(127,127,127,.06);border-radius:0 6px 6px 0}.omni-latex__theorem-title{margin-bottom:.5em;font-weight:600}.omni-latex__theorem-note{margin-left:6px;font-weight:400;color:var(--tex-muted)}.omni-latex__theorem-body{font-style:italic}.omni-latex__theorem-body p:last-child{margin-bottom:0}\n.omni-latex__verbatim{margin:0 0 .9em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px;background:rgba(127,127,127,.08);font:12px/1.5 var(--omni-mono,monospace);white-space:pre;overflow-x:auto}\n.omni-latex__unsupported{margin:0 0 .9em;border:1px dashed var(--tex-border);border-radius:6px;background:rgba(127,127,127,.06);overflow:hidden}.omni-latex__badge{display:block;padding:6px 12px;border-bottom:1px dashed var(--tex-border);color:var(--omni-warning,#ffd98a);font-size:11px;font-weight:600;letter-spacing:.02em}.omni-latex__unsupported pre{margin:0;padding:12px 14px;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto;color:var(--tex-muted)}.omni-latex__unsupported--inline{display:inline;border:0;background:none;padding:0 2px;color:var(--tex-muted);font:12px/1.5 var(--omni-mono,monospace)}\n.omni-latex__float{margin:0 0 1em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px}.omni-latex__float-body{display:flex;flex-direction:column;align-items:center}.omni-latex__float-body>*{max-width:100%}.omni-latex__float-caption{margin-top:.6em;color:var(--tex-muted);font-size:.94em;text-align:center}\n.omni-latex__source{flex:1;width:100%;min-height:62vh;resize:none;border:0;outline:0;padding:16px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e));color:inherit;font:13px/1.55 var(--omni-mono,monospace);tab-size:4}\n.omni-latex__warning{padding:10px 12px;border:1px solid #b98b2f;border-radius:6px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);white-space:pre-wrap}.omni-latex__warning[hidden]{display:none}.omni-latex__message{padding:10px 12px;border:1px solid var(--omni-error,#f44747);border-radius:6px;color:var(--omni-error,#f44747);white-space:pre-wrap}.omni-latex__message[hidden]{display:none}\n@media(max-width:900px){.omni-latex__workspace,.omni-latex__workspace.is-split{grid-template-columns:1fr}}\n";
|
|
@@ -38,7 +38,7 @@ export const latexViewerCss = `
|
|
|
38
38
|
.omni-latex__theorem{margin:0 0 1em;padding:12px 14px;border-left:3px solid var(--tex-border);background:rgba(127,127,127,.06);border-radius:0 6px 6px 0}.omni-latex__theorem-title{margin-bottom:.5em;font-weight:600}.omni-latex__theorem-note{margin-left:6px;font-weight:400;color:var(--tex-muted)}.omni-latex__theorem-body{font-style:italic}.omni-latex__theorem-body p:last-child{margin-bottom:0}
|
|
39
39
|
.omni-latex__verbatim{margin:0 0 .9em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px;background:rgba(127,127,127,.08);font:12px/1.5 var(--omni-mono,monospace);white-space:pre;overflow-x:auto}
|
|
40
40
|
.omni-latex__unsupported{margin:0 0 .9em;border:1px dashed var(--tex-border);border-radius:6px;background:rgba(127,127,127,.06);overflow:hidden}.omni-latex__badge{display:block;padding:6px 12px;border-bottom:1px dashed var(--tex-border);color:var(--omni-warning,#ffd98a);font-size:11px;font-weight:600;letter-spacing:.02em}.omni-latex__unsupported pre{margin:0;padding:12px 14px;font:12px/1.5 var(--omni-mono,monospace);white-space:pre-wrap;overflow-x:auto;color:var(--tex-muted)}.omni-latex__unsupported--inline{display:inline;border:0;background:none;padding:0 2px;color:var(--tex-muted);font:12px/1.5 var(--omni-mono,monospace)}
|
|
41
|
-
.omni-latex__float{margin:0 0
|
|
41
|
+
.omni-latex__float{margin:0 0 1em;padding:12px 14px;border:1px solid var(--tex-border);border-radius:6px}.omni-latex__float-body{display:flex;flex-direction:column;align-items:center}.omni-latex__float-body>*{max-width:100%}.omni-latex__float-caption{margin-top:.6em;color:var(--tex-muted);font-size:.94em;text-align:center}
|
|
42
42
|
.omni-latex__source{flex:1;width:100%;min-height:62vh;resize:none;border:0;outline:0;padding:16px;background:var(--omni-editor-bg,var(--omni-bg,#1e1e1e));color:inherit;font:13px/1.55 var(--omni-mono,monospace);tab-size:4}
|
|
43
43
|
.omni-latex__warning{padding:10px 12px;border:1px solid #b98b2f;border-radius:6px;background:rgba(185,139,47,.14);color:var(--omni-warning,#ffd98a);white-space:pre-wrap}.omni-latex__warning[hidden]{display:none}.omni-latex__message{padding:10px 12px;border:1px solid var(--omni-error,#f44747);border-radius:6px;color:var(--omni-error,#f44747);white-space:pre-wrap}.omni-latex__message[hidden]{display:none}
|
|
44
44
|
@media(max-width:900px){.omni-latex__workspace,.omni-latex__workspace.is-split{grid-template-columns:1fr}}
|
|
@@ -18,8 +18,13 @@ export type MarkdownAction = {
|
|
|
18
18
|
type: 'undo';
|
|
19
19
|
} | {
|
|
20
20
|
type: 'redo';
|
|
21
|
-
}
|
|
21
|
+
}
|
|
22
|
+
/** `source` is the text that actually reached the file. Writes are async and
|
|
23
|
+
* the editor stays live during one, so the current source at completion is
|
|
24
|
+
* not necessarily what was written. Omitted = "what is in the editor now". */
|
|
25
|
+
| {
|
|
22
26
|
type: 'mark-saved';
|
|
27
|
+
source?: string;
|
|
23
28
|
} | {
|
|
24
29
|
type: 'copy-code';
|
|
25
30
|
code: string;
|
|
@@ -52,7 +52,8 @@ export function createMarkdownController(text, _headings) {
|
|
|
52
52
|
setSource(redo.pop());
|
|
53
53
|
}
|
|
54
54
|
else if (action.type === 'mark-saved') {
|
|
55
|
-
|
|
55
|
+
const savedSource = action.source ?? state.source;
|
|
56
|
+
state = { ...state, savedSource, dirty: state.source !== savedSource };
|
|
56
57
|
}
|
|
57
58
|
emit();
|
|
58
59
|
},
|
|
@@ -56,6 +56,11 @@ export type MarkdownViewerContext = HostContext & {
|
|
|
56
56
|
writeback?: FileWritebackService;
|
|
57
57
|
save?: FileSaveService;
|
|
58
58
|
};
|
|
59
|
+
/** Markdown mount handle: `isDirty()` lets a host guard a re-mount against
|
|
60
|
+
* discarding unsaved source edits (DESIGN.md §3-②). */
|
|
61
|
+
export interface MarkdownViewerHandle extends ViewerHandle {
|
|
62
|
+
isDirty(): boolean;
|
|
63
|
+
}
|
|
59
64
|
export interface MarkdownMountOptions extends MountOptions {
|
|
60
65
|
limits?: ResourceLimits;
|
|
61
66
|
markdownLimits?: MarkdownParseOptions['markdownLimits'];
|
|
@@ -64,4 +69,4 @@ export interface MarkdownMountOptions extends MountOptions {
|
|
|
64
69
|
* wants it configurable should drive this from its own settings. */
|
|
65
70
|
scrollSync?: boolean;
|
|
66
71
|
}
|
|
67
|
-
export declare function mountMarkdownViewer(input: ViewerInput, container: HTMLElement, ctx: MarkdownViewerContext, deps: MarkdownViewerDeps, options?: MarkdownMountOptions): Promise<
|
|
72
|
+
export declare function mountMarkdownViewer(input: ViewerInput, container: HTMLElement, ctx: MarkdownViewerContext, deps: MarkdownViewerDeps, options?: MarkdownMountOptions): Promise<MarkdownViewerHandle>;
|
|
@@ -546,13 +546,20 @@ export async function mountMarkdownViewer(input, container, ctx, deps, options =
|
|
|
546
546
|
}
|
|
547
547
|
};
|
|
548
548
|
const saveSource = async () => {
|
|
549
|
-
|
|
549
|
+
// Snapshot once: the write is async and the editor stays live, so
|
|
550
|
+
// `controller.state.source` after the await may already be a later edit
|
|
551
|
+
// than the bytes that reached the file.
|
|
552
|
+
const saved = controller.state.source;
|
|
553
|
+
const bytes = new TextEncoder().encode(saved);
|
|
550
554
|
if (ctx.writeback) {
|
|
555
|
+
// The caption is the dirty indicator: leave syncState's verdict alone
|
|
556
|
+
// when an edit arrived mid-write and the source is still unsaved.
|
|
551
557
|
try {
|
|
552
558
|
await ctx.writeback.write(bytes);
|
|
553
|
-
controller.dispatch({ type: 'mark-saved' });
|
|
559
|
+
controller.dispatch({ type: 'mark-saved', source: saved });
|
|
554
560
|
setStatus('common.savedToOriginal', 'valid');
|
|
555
|
-
|
|
561
|
+
if (!controller.state.dirty)
|
|
562
|
+
sourceCaption.textContent = t('common.savedToOriginal');
|
|
556
563
|
}
|
|
557
564
|
catch (error) {
|
|
558
565
|
ctx.logger.log('error', `markdown save failed: ${String(error)}`);
|
|
@@ -739,9 +746,15 @@ export async function mountMarkdownViewer(input, container, ctx, deps, options =
|
|
|
739
746
|
releaseAssets();
|
|
740
747
|
throw new MountAbortedError();
|
|
741
748
|
}
|
|
742
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
dispose() { disposed = true; renderVersion++; if (liveTimer)
|
|
743
751
|
clearTimeout(liveTimer); releaseAssets(); for (const dispose of disposers.splice(0))
|
|
744
|
-
dispose(); shell.remove(); }
|
|
752
|
+
dispose(); shell.remove(); },
|
|
753
|
+
// The controller, not the textarea: `mark-saved` (a successful writeback)
|
|
754
|
+
// is the only thing that clears it, so a failed save or a download copy
|
|
755
|
+
// stays dirty.
|
|
756
|
+
isDirty: () => controller.state.dirty
|
|
757
|
+
};
|
|
745
758
|
}
|
|
746
759
|
/** Flat text for an outline entry. Rendered math contributes twice to
|
|
747
760
|
* `textContent` — KaTeX emits a visual copy alongside a MathML twin — so the
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { diagramViewerCss, type DiagramMountOptions, type DiagramViewerContext } from '../diagram.js';
|
|
2
|
-
import type {
|
|
1
|
+
import { diagramViewerCss, type DiagramMountOptions, type DiagramViewerContext, type DiagramViewerHandle } from '../diagram.js';
|
|
2
|
+
import type { ViewerInput } from '../types.js';
|
|
3
3
|
export { parseMermaid } from '../../parsers/mermaid/index.js';
|
|
4
4
|
export { diagramViewerCss as mermaidViewerCss };
|
|
5
5
|
export type MermaidViewerContext = DiagramViewerContext;
|
|
6
6
|
export type MermaidMountOptions = DiagramMountOptions;
|
|
7
|
+
export type MermaidViewerHandle = DiagramViewerHandle;
|
|
7
8
|
export declare const MERMAID_VIEWER_META: {
|
|
8
9
|
id: string;
|
|
9
10
|
displayNameKey: string;
|
|
@@ -13,4 +14,4 @@ export declare const MERMAID_VIEWER_META: {
|
|
|
13
14
|
optionalServices: readonly ["clipboard", "writeback", "save"];
|
|
14
15
|
inputOwnership: "borrows";
|
|
15
16
|
};
|
|
16
|
-
export declare function mountMermaidViewer(input: ViewerInput, container: HTMLElement, ctx: MermaidViewerContext, options?: MermaidMountOptions): Promise<
|
|
17
|
+
export declare function mountMermaidViewer(input: ViewerInput, container: HTMLElement, ctx: MermaidViewerContext, options?: MermaidMountOptions): Promise<MermaidViewerHandle>;
|
|
@@ -123,8 +123,18 @@ export type PdfAction = {
|
|
|
123
123
|
type: 'undo';
|
|
124
124
|
} | {
|
|
125
125
|
type: 'redo';
|
|
126
|
-
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `savedState` is the page order and annotations that actually reached the
|
|
129
|
+
* file. Builds and writes are async and the document stays editable during
|
|
130
|
+
* one, so the state at completion is not necessarily what was written —
|
|
131
|
+
* omitting it would mark an edit made mid-save as saved and let a host
|
|
132
|
+
* discard it. Omitted = "the current state", which is only correct for a
|
|
133
|
+
* synchronous save.
|
|
134
|
+
*/
|
|
135
|
+
| {
|
|
127
136
|
type: 'mark-saved';
|
|
137
|
+
savedState?: Pick<PdfViewState, 'pageOrder' | 'annotations'>;
|
|
128
138
|
};
|
|
129
139
|
export interface PdfController {
|
|
130
140
|
readonly state: PdfViewState;
|
|
@@ -217,7 +217,14 @@ export function createPdfController(pageCount, seed, options = {}) {
|
|
|
217
217
|
selectedAnnotationId = action.id;
|
|
218
218
|
break;
|
|
219
219
|
case 'mark-saved':
|
|
220
|
-
|
|
220
|
+
// Same shape as `signature()` so an unchanged document still
|
|
221
|
+
// compares equal (state copies preserve key order).
|
|
222
|
+
savedSignature = action.savedState
|
|
223
|
+
? JSON.stringify({
|
|
224
|
+
pageOrder: action.savedState.pageOrder,
|
|
225
|
+
annotations: action.savedState.annotations
|
|
226
|
+
})
|
|
227
|
+
: signature();
|
|
221
228
|
break;
|
|
222
229
|
}
|
|
223
230
|
if (before && beforeSignature !== signature()) {
|
|
@@ -1757,13 +1757,20 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
|
|
|
1757
1757
|
async function save() {
|
|
1758
1758
|
if (!ctx.writeback || saveBtn.disabled)
|
|
1759
1759
|
return;
|
|
1760
|
+
// Pin what is being written before the build starts: only the save/merge
|
|
1761
|
+
// buttons are disabled while an operation runs, so pages and annotations
|
|
1762
|
+
// can still change across the await. Taking the state at completion would
|
|
1763
|
+
// mark those changes as saved and let a host discard them on re-mount.
|
|
1764
|
+
// `stateForSave()` only rasterizes text for output, so the pre-raster
|
|
1765
|
+
// state is what this write represents.
|
|
1766
|
+
const { pageOrder, annotations } = controller.state;
|
|
1760
1767
|
const result = await runOperation('save', async (control) => {
|
|
1761
1768
|
const data = await buildOutput(stateForSave(), control);
|
|
1762
1769
|
await ctx.writeback.write(data);
|
|
1763
1770
|
});
|
|
1764
1771
|
if (result.status === 'succeeded') {
|
|
1765
1772
|
mergedSinceSave = false;
|
|
1766
|
-
controller.dispatch({ type: 'mark-saved' });
|
|
1773
|
+
controller.dispatch({ type: 'mark-saved', savedState: { pageOrder, annotations } });
|
|
1767
1774
|
refreshChrome();
|
|
1768
1775
|
status.textContent = t('common.savedToOriginal');
|
|
1769
1776
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { diagramViewerCss, type DiagramMountOptions, type DiagramViewerContext } from '../diagram.js';
|
|
2
|
-
import type {
|
|
1
|
+
import { diagramViewerCss, type DiagramMountOptions, type DiagramViewerContext, type DiagramViewerHandle } from '../diagram.js';
|
|
2
|
+
import type { ViewerInput } from '../types.js';
|
|
3
3
|
export { parsePlantUml } from '../../parsers/plantuml/index.js';
|
|
4
4
|
export { diagramViewerCss as plantumlViewerCss };
|
|
5
5
|
export type PlantUmlViewerContext = DiagramViewerContext;
|
|
6
6
|
export type PlantUmlMountOptions = DiagramMountOptions;
|
|
7
|
+
export type PlantUmlViewerHandle = DiagramViewerHandle;
|
|
7
8
|
export declare const PLANTUML_VIEWER_META: {
|
|
8
9
|
id: string;
|
|
9
10
|
displayNameKey: string;
|
|
@@ -13,4 +14,4 @@ export declare const PLANTUML_VIEWER_META: {
|
|
|
13
14
|
optionalServices: readonly ["clipboard", "writeback", "save"];
|
|
14
15
|
inputOwnership: "borrows";
|
|
15
16
|
};
|
|
16
|
-
export declare function mountPlantUmlViewer(input: ViewerInput, container: HTMLElement, ctx: PlantUmlViewerContext, options?: PlantUmlMountOptions): Promise<
|
|
17
|
+
export declare function mountPlantUmlViewer(input: ViewerInput, container: HTMLElement, ctx: PlantUmlViewerContext, options?: PlantUmlMountOptions): Promise<PlantUmlViewerHandle>;
|