omni-viewer-core 0.3.0 → 0.5.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 (36) hide show
  1. package/README.md +28 -0
  2. package/dist/host/index.d.ts +14 -2
  3. package/dist/i18n/catalog.en.js +8 -0
  4. package/dist/i18n/catalog.ja.js +9 -1
  5. package/dist/i18n/catalog.ko.js +8 -0
  6. package/dist/i18n/catalog.zh-cn.js +8 -1
  7. package/dist/parsers/pptx/index.d.ts +1 -0
  8. package/dist/parsers/pptx/index.js +1 -1
  9. package/dist/parsers/yaml/index.js +14 -2
  10. package/dist/parsers/yaml/self-loading.js +26 -14
  11. package/dist/registry/index.js +1 -1
  12. package/dist/styles/archive.css +1 -1
  13. package/dist/styles/pdf.css +26 -0
  14. package/dist/styles/toml.css +1 -1
  15. package/dist/styles/yaml.css +1 -1
  16. package/dist/viewers/archive/index.js +60 -11
  17. package/dist/viewers/archive/styles.d.ts +1 -1
  18. package/dist/viewers/archive/styles.js +1 -1
  19. package/dist/viewers/json/controller.js +13 -24
  20. package/dist/viewers/json/index.js +5 -1
  21. package/dist/viewers/markdown/index.d.ts +3 -2
  22. package/dist/viewers/markdown/index.js +29 -14
  23. package/dist/viewers/pdf/controller.d.ts +14 -2
  24. package/dist/viewers/pdf/controller.js +28 -10
  25. package/dist/viewers/pdf/index.d.ts +79 -8
  26. package/dist/viewers/pdf/index.js +363 -81
  27. package/dist/viewers/pdf/self-loading.d.ts +3 -3
  28. package/dist/viewers/pdf/styles.d.ts +1 -1
  29. package/dist/viewers/pdf/styles.js +26 -0
  30. package/dist/viewers/ppt/index.js +3 -3
  31. package/dist/viewers/structured-styles.d.ts +1 -1
  32. package/dist/viewers/structured-styles.js +1 -1
  33. package/dist/viewers/structured.d.ts +1 -0
  34. package/dist/viewers/structured.js +5 -3
  35. package/dist/viewers/yaml/controller.js +48 -5
  36. package/package.json +2 -1
@@ -246,10 +246,13 @@ export async function mountJsonViewer(input, container, ctx, options = {}) {
246
246
  copyResultBtn.disabled = true;
247
247
  copyResultBtn.title = t('common.noClipboard');
248
248
  }
249
+ const replaceResultBtn = el('button', undefined, t('json.result.replace'));
250
+ replaceResultBtn.type = 'button';
251
+ on(replaceResultBtn, 'click', () => controller.dispatch({ type: 'apply-result-to-editor' }));
249
252
  const closeResultBtn = el('button', undefined, t('json.result.close'));
250
253
  closeResultBtn.type = 'button';
251
254
  on(closeResultBtn, 'click', () => controller.dispatch({ type: 'dismiss-result' }));
252
- resultActions.append(copyResultBtn, closeResultBtn);
255
+ resultActions.append(copyResultBtn, replaceResultBtn, closeResultBtn);
253
256
  resultPanel.append(resultTitle, resultOutput, resultMarkup, resultTableWrap, resultActions);
254
257
  frame.append(toolbar, statusBar, diagnosticsBar, resultPanel, body);
255
258
  root.appendChild(frame);
@@ -475,6 +478,7 @@ export async function mountJsonViewer(input, container, ctx, options = {}) {
475
478
  if (isMarkup)
476
479
  renderMarkupResult(state.toolResult.action, state.toolResult.output);
477
480
  copyResultBtn.disabled = !clipboard || !!state.toolResult.error;
481
+ replaceResultBtn.disabled = !!state.toolResult.error;
478
482
  }
479
483
  else {
480
484
  resultPanel.style.display = 'none';
@@ -1,4 +1,4 @@
1
- import type { ClipboardService, DocumentAssetsService, FileWritebackService, HostContext, NavigationService } from '../../host/index.js';
1
+ import type { ClipboardService, DocumentAssetsService, FileSaveService, FileWritebackService, HostContext, NavigationService } from '../../host/index.js';
2
2
  import { type MarkdownParseOptions } from '../../parsers/markdown/index.js';
3
3
  import type { ResourceLimits } from '../../parsers/types.js';
4
4
  import { type MountOptions, type ViewerHandle, type ViewerInput } from '../types.js';
@@ -12,7 +12,7 @@ export declare const MARKDOWN_VIEWER_META: {
12
12
  extensions: string[];
13
13
  priority: number;
14
14
  requiredServices: readonly [];
15
- optionalServices: readonly ["clipboard", "navigation", "documentAssets", "writeback"];
15
+ optionalServices: readonly ["clipboard", "navigation", "documentAssets", "writeback", "save"];
16
16
  inputOwnership: "borrows";
17
17
  };
18
18
  export interface MarkdownRenderer {
@@ -56,6 +56,7 @@ export type MarkdownViewerContext = HostContext & {
56
56
  navigation?: NavigationService;
57
57
  documentAssets?: DocumentAssetsService;
58
58
  writeback?: FileWritebackService;
59
+ save?: FileSaveService;
59
60
  };
60
61
  export interface MarkdownMountOptions extends MountOptions {
61
62
  limits?: ResourceLimits;
@@ -12,7 +12,7 @@ export const MARKDOWN_VIEWER_META = {
12
12
  id: 'markdown', displayNameKey: 'markdown.title',
13
13
  extensions: ['md', 'markdown', 'mdown', 'mkdn', 'mkd'], priority: 10,
14
14
  requiredServices: [],
15
- optionalServices: ['clipboard', 'navigation', 'documentAssets', 'writeback'],
15
+ optionalServices: ['clipboard', 'navigation', 'documentAssets', 'writeback', 'save'],
16
16
  inputOwnership: 'borrows'
17
17
  };
18
18
  const SANITIZE = {
@@ -323,22 +323,37 @@ export async function mountMarkdownViewer(input, container, ctx, deps, options =
323
323
  }
324
324
  };
325
325
  const saveSource = async () => {
326
- if (!ctx.writeback) {
327
- showMessage(t('common.noWriteback'));
328
- setStatus('common.saveFailed', 'invalid');
326
+ const bytes = new TextEncoder().encode(controller.state.source);
327
+ if (ctx.writeback) {
328
+ try {
329
+ await ctx.writeback.write(bytes);
330
+ controller.dispatch({ type: 'mark-saved' });
331
+ setStatus('common.savedToOriginal', 'valid');
332
+ sourceCaption.textContent = t('common.savedToOriginal');
333
+ }
334
+ catch (error) {
335
+ ctx.logger.log('error', `markdown save failed: ${String(error)}`);
336
+ setStatus('common.saveFailed', 'invalid');
337
+ showMessage(errorText(error, t('common.saveFailed')));
338
+ }
329
339
  return;
330
340
  }
331
- try {
332
- await ctx.writeback.write(new TextEncoder().encode(controller.state.source));
333
- controller.dispatch({ type: 'mark-saved' });
334
- setStatus('common.savedToOriginal', 'valid');
335
- sourceCaption.textContent = t('common.savedToOriginal');
336
- }
337
- catch (error) {
338
- ctx.logger.log('error', `markdown save failed: ${String(error)}`);
339
- setStatus('common.saveFailed', 'invalid');
340
- showMessage(errorText(error, t('common.saveFailed')));
341
+ if (ctx.save) {
342
+ // Download fallback saves a copy, not the original — the dirty
343
+ // state is intentionally kept (no mark-saved).
344
+ try {
345
+ await ctx.save.saveFile(input.fileName, bytes, 'text/markdown');
346
+ setStatus('common.savedToOriginal', 'valid');
347
+ }
348
+ catch (error) {
349
+ ctx.logger.log('error', `markdown save failed: ${String(error)}`);
350
+ setStatus('common.saveFailed', 'invalid');
351
+ showMessage(errorText(error, t('common.saveFailed')));
352
+ }
353
+ return;
341
354
  }
355
+ showMessage(t('common.noWriteback'));
356
+ setStatus('common.saveFailed', 'invalid');
342
357
  };
343
358
  const copy = async (value, successKey) => {
344
359
  if (!ctx.clipboard)
@@ -1,4 +1,6 @@
1
- export declare const PDF_ZOOM_LEVELS: readonly [50, 75, 100, 125, 150, 200, 300];
1
+ export declare const PDF_ZOOM_LEVELS: readonly [50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300];
2
+ export declare const PDF_MIN_ZOOM = 25;
3
+ export declare const PDF_MAX_ZOOM = 400;
2
4
  export type PdfZoom = number;
3
5
  export interface PdfTextAnnotation {
4
6
  id: string;
@@ -109,12 +111,22 @@ export type PdfAction = {
109
111
  };
110
112
  export interface PdfController {
111
113
  readonly state: PdfViewState;
114
+ /** Ordered button steps used by zoom-in/zoom-out. */
115
+ readonly zoomLevels: readonly number[];
116
+ readonly minZoom: number;
117
+ readonly maxZoom: number;
112
118
  dispatch(action: PdfAction): void;
113
119
  subscribe(listener: (state: PdfViewState) => void): () => void;
114
120
  }
121
+ export interface PdfControllerOptions {
122
+ /** Custom button steps. Fit-to-width/height may still set an intermediate value. */
123
+ zoomLevels?: readonly number[];
124
+ minZoom?: number;
125
+ maxZoom?: number;
126
+ }
115
127
  /** Persisted layer used to restore an editing session (sidecar rehydration). */
116
128
  export interface PdfControllerSeed {
117
129
  pageOrder?: readonly number[];
118
130
  annotations?: readonly PdfAnnotation[];
119
131
  }
120
- export declare function createPdfController(pageCount: number, seed?: PdfControllerSeed): PdfController;
132
+ export declare function createPdfController(pageCount: number, seed?: PdfControllerSeed, options?: PdfControllerOptions): PdfController;
@@ -1,4 +1,8 @@
1
- export const PDF_ZOOM_LEVELS = [50, 75, 100, 125, 150, 200, 300];
1
+ export const PDF_ZOOM_LEVELS = [
2
+ 50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300
3
+ ];
4
+ export const PDF_MIN_ZOOM = 25;
5
+ export const PDF_MAX_ZOOM = 400;
2
6
  /**
3
7
  * PDF.js commonly returns one client rect per text span, even for a single
4
8
  * continuous selection. Join neighbouring spans on the same text line so
@@ -41,8 +45,18 @@ export function mergeHighlightRects(rects) {
41
45
  return merged;
42
46
  });
43
47
  }
44
- function nearestZoom(value) {
45
- return PDF_ZOOM_LEVELS.reduce((best, candidate) => Math.abs(candidate - value) < Math.abs(best - value) ? candidate : best);
48
+ function normalizedZoomOptions(options) {
49
+ const requestedMin = Number.isFinite(options.minZoom) ? Math.round(options.minZoom) : PDF_MIN_ZOOM;
50
+ const requestedMax = Number.isFinite(options.maxZoom) ? Math.round(options.maxZoom) : PDF_MAX_ZOOM;
51
+ const min = Math.max(1, Math.min(requestedMin, requestedMax));
52
+ const max = Math.max(min, Math.max(requestedMin, requestedMax));
53
+ const source = options.zoomLevels ?? PDF_ZOOM_LEVELS;
54
+ const levels = [...new Set(source
55
+ .filter(Number.isFinite)
56
+ .map((level) => Math.round(level))
57
+ .filter((level) => level >= min && level <= max))]
58
+ .sort((a, b) => a - b);
59
+ return { levels: levels.length > 0 ? levels : [min, max], min, max };
46
60
  }
47
61
  /** Highest `pdf-a<n>` suffix among seeded ids, so new ids never collide. */
48
62
  function maxSequence(annotations) {
@@ -59,10 +73,11 @@ function copyAnnotations(annotations) {
59
73
  ? { ...annotation, rects: annotation.rects.map((rect) => ({ ...rect })) }
60
74
  : { ...annotation });
61
75
  }
62
- export function createPdfController(pageCount, seed) {
76
+ export function createPdfController(pageCount, seed, options = {}) {
63
77
  const original = Array.from({ length: Math.max(0, Math.floor(pageCount)) }, (_, i) => i + 1);
64
78
  const listeners = new Set();
65
- let zoom = 100;
79
+ const zoomOptions = normalizedZoomOptions(options);
80
+ let zoom = Math.max(zoomOptions.min, Math.min(zoomOptions.max, 100));
66
81
  // A seeded order may only reference pages that still exist; an empty result
67
82
  // falls back to the natural order so the document is never left blank.
68
83
  const seededOrder = seed?.pageOrder?.filter((page) => page >= 1 && page <= original.length) ?? [];
@@ -95,6 +110,9 @@ export function createPdfController(pageCount, seed) {
95
110
  savedSignature = signature();
96
111
  return {
97
112
  get state() { return state(); },
113
+ zoomLevels: zoomOptions.levels,
114
+ minZoom: zoomOptions.min,
115
+ maxZoom: zoomOptions.max,
98
116
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
99
117
  dispatch(action) {
100
118
  if (action.type === 'undo') {
@@ -125,19 +143,19 @@ export function createPdfController(pageCount, seed) {
125
143
  const beforeSignature = tracksHistory ? signature() : undefined;
126
144
  switch (action.type) {
127
145
  case 'zoom-in': {
128
- zoom = PDF_ZOOM_LEVELS.find((level) => level > zoom)
129
- ?? PDF_ZOOM_LEVELS[PDF_ZOOM_LEVELS.length - 1];
146
+ zoom = zoomOptions.levels.find((level) => level > zoom)
147
+ ?? zoomOptions.max;
130
148
  break;
131
149
  }
132
150
  case 'zoom-out': {
133
- zoom = [...PDF_ZOOM_LEVELS].reverse().find((level) => level < zoom)
134
- ?? PDF_ZOOM_LEVELS[0];
151
+ zoom = [...zoomOptions.levels].reverse().find((level) => level < zoom)
152
+ ?? zoomOptions.min;
135
153
  break;
136
154
  }
137
155
  case 'set-zoom': {
138
156
  if (!Number.isFinite(action.zoom))
139
157
  return;
140
- zoom = Math.max(25, Math.min(400, Math.round(action.zoom)));
158
+ zoom = Math.max(zoomOptions.min, Math.min(zoomOptions.max, Math.round(action.zoom)));
141
159
  break;
142
160
  }
143
161
  case 'reorder-pages': {
@@ -1,12 +1,73 @@
1
- import type { FilePickService, FileSaveService, FileWritebackService, HostContext } from '../../host/index.js';
1
+ import type { FilePickService, FileSaveResult, FileSaveService, FileWritebackService, HostContext } from '../../host/index.js';
2
2
  import { type MountOptions, type ViewerHandle, type ViewerInput } from '../types.js';
3
- import { type PdfController } from './controller.js';
3
+ import { type PdfController, type PdfControllerOptions, type PdfViewState } from './controller.js';
4
4
  import { type PdfLibModule } from './editing.js';
5
- export { createPdfController, PDF_ZOOM_LEVELS } from './controller.js';
6
- export type { PdfController, PdfAction, PdfViewState, PdfAnnotation } from './controller.js';
5
+ export { createPdfController, PDF_MAX_ZOOM, PDF_MIN_ZOOM, PDF_ZOOM_LEVELS } from './controller.js';
6
+ export type { PdfController, PdfControllerOptions, PdfAction, PdfViewState, PdfAnnotation } from './controller.js';
7
7
  export { pdfViewerCss } from './styles.js';
8
- export { buildEditedPdf, mergePdfBytes, savedPdfName } from './editing.js';
9
- export type { PdfLibModule } from './editing.js';
8
+ export { buildEditedPdf, buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_BASE_NAME, SIDECAR_LAYER_NAME } from './editing.js';
9
+ export type { ParsedLayer, PdfLibModule } from './editing.js';
10
+ /** Asset key passed to `HostContext.assets.resolveAssetUrl` by default. */
11
+ export declare const PDF_WORKER_ASSET_KEY = "assets/pdfjs/pdf.worker.min.mjs";
12
+ export type PdfSaveMode = 'hybrid' | 'flattened';
13
+ export type PdfProcessingMode = 'auto' | 'host' | 'browser';
14
+ export interface PdfToolbarAction {
15
+ id: string;
16
+ label: string;
17
+ title?: string;
18
+ ariaLabel?: string;
19
+ disabled?: boolean | (() => boolean);
20
+ onClick(): void | Promise<void>;
21
+ }
22
+ export interface PdfProcessingControl {
23
+ signal: AbortSignal;
24
+ onProgress(progress: number | undefined): void;
25
+ }
26
+ /** Optional host/Worker implementation for byte-heavy PDF operations. */
27
+ export interface PdfProcessingService {
28
+ /** Request byte arrays are borrowed: implementations must not mutate or
29
+ * detach their buffers. A Worker adapter may copy/transfer internally. */
30
+ buildPdf?(request: {
31
+ source: Uint8Array;
32
+ state: PdfViewState;
33
+ mode: PdfSaveMode;
34
+ }, control: PdfProcessingControl): Promise<Uint8Array>;
35
+ mergePdfs?(request: {
36
+ first: Uint8Array;
37
+ second: Uint8Array;
38
+ }, control: PdfProcessingControl): Promise<Uint8Array>;
39
+ }
40
+ export type PdfOperationState = {
41
+ status: 'idle';
42
+ } | {
43
+ status: 'running';
44
+ kind: 'save' | 'save-as' | 'merge';
45
+ progress?: number;
46
+ } | {
47
+ status: 'succeeded';
48
+ kind: 'save' | 'save-as' | 'merge';
49
+ } | {
50
+ status: 'failed';
51
+ kind: 'save' | 'save-as' | 'merge';
52
+ error: string;
53
+ } | {
54
+ status: 'cancelled';
55
+ kind: 'save' | 'save-as' | 'merge';
56
+ };
57
+ export interface PdfMountOptions extends MountOptions, PdfControllerOptions {
58
+ /** Defaults to `hybrid` for v0.4 compatibility. */
59
+ saveMode?: PdfSaveMode;
60
+ /** `auto` delegates when a service method exists, `browser` always uses pdf-lib. */
61
+ processingMode?: PdfProcessingMode;
62
+ /** Maximum merge input requested from FilePickService. */
63
+ maxMergeBytes?: number;
64
+ toolbarActions?: readonly PdfToolbarAction[];
65
+ /** Safe default for VS Code CSP is false. */
66
+ isEvalSupported?: boolean;
67
+ /** Host-provided worker URL. Omit to resolve {@link PDF_WORKER_ASSET_KEY}. */
68
+ workerSrc?: string;
69
+ onSaveAsComplete?(result: FileSaveResult | void): void | Promise<void>;
70
+ }
10
71
  /** Viewer metadata — single source for the registry codegen (DESIGN.md §7). */
11
72
  export declare const PDF_VIEWER_META: {
12
73
  id: string;
@@ -58,12 +119,13 @@ export interface PdfJsDocument {
58
119
  }
59
120
  export interface PdfJsLoadingTask {
60
121
  promise: Promise<PdfJsDocument>;
61
- destroy(): void;
122
+ destroy(): void | Promise<void>;
62
123
  }
63
124
  export interface PdfJsModule {
64
125
  getDocument(options: {
65
126
  data: Uint8Array;
66
127
  password?: string;
128
+ isEvalSupported?: boolean;
67
129
  }): PdfJsLoadingTask;
68
130
  GlobalWorkerOptions: {
69
131
  workerSrc: string;
@@ -80,9 +142,18 @@ export interface PdfViewerDeps {
80
142
  /** Editing dependency (save / save-as / merge / annotation stamping).
81
143
  * Absent -> editing controls disabled with a reason. */
82
144
  loadPdfLib?(): Promise<PdfLibModule>;
145
+ /** Host/extension-host/Web Worker implementation for large byte operations. */
146
+ processing?: PdfProcessingService;
83
147
  }
84
148
  export interface PdfViewerHandle extends ViewerHandle {
85
149
  readonly controller: PdfController;
150
+ readonly operation: PdfOperationState;
86
151
  isDirty(): boolean;
152
+ cancelOperation(): void;
153
+ refreshToolbarActions(): void;
87
154
  }
88
- export declare function mountPdfViewer(input: ViewerInput, container: HTMLElement, ctx: PdfViewerContext, deps: PdfViewerDeps, options?: MountOptions): Promise<PdfViewerHandle>;
155
+ export declare function containPdfSignatureSize(width: number, height: number, maxWidth?: number, maxHeight?: number): {
156
+ width: number;
157
+ height: number;
158
+ };
159
+ export declare function mountPdfViewer(input: ViewerInput, container: HTMLElement, ctx: PdfViewerContext, deps: PdfViewerDeps, options?: PdfMountOptions): Promise<PdfViewerHandle>;