omni-viewer-core 0.4.0 → 0.6.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 (43) hide show
  1. package/README.md +29 -1
  2. package/dist/host/index.d.ts +14 -2
  3. package/dist/i18n/catalog.en.js +21 -0
  4. package/dist/i18n/catalog.ja.js +22 -2
  5. package/dist/i18n/catalog.ko.js +21 -0
  6. package/dist/i18n/catalog.zh-cn.js +18 -1
  7. package/dist/parsers/safetensors/index.d.ts +34 -0
  8. package/dist/parsers/safetensors/index.js +233 -0
  9. package/dist/parsers/toml/model.d.ts +10 -0
  10. package/dist/parsers/toml/parse.js +104 -34
  11. package/dist/parsers/yaml/index.js +14 -2
  12. package/dist/parsers/yaml/self-loading.js +26 -14
  13. package/dist/registry/index.d.ts +1 -0
  14. package/dist/registry/index.js +6 -2
  15. package/dist/styles/pdf.css +203 -0
  16. package/dist/styles/safetensors.css +3 -0
  17. package/dist/styles/toml.css +1 -1
  18. package/dist/styles/yaml.css +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 +22 -2
  24. package/dist/viewers/pdf/controller.js +32 -10
  25. package/dist/viewers/pdf/editing.d.ts +11 -0
  26. package/dist/viewers/pdf/editing.js +19 -16
  27. package/dist/viewers/pdf/index.d.ts +81 -9
  28. package/dist/viewers/pdf/index.js +558 -104
  29. package/dist/viewers/pdf/self-loading.d.ts +3 -3
  30. package/dist/viewers/pdf/styles.d.ts +1 -1
  31. package/dist/viewers/pdf/styles.js +203 -0
  32. package/dist/viewers/safetensors/index.d.ts +19 -0
  33. package/dist/viewers/safetensors/index.js +170 -0
  34. package/dist/viewers/safetensors/styles.d.ts +1 -0
  35. package/dist/viewers/safetensors/styles.js +4 -0
  36. package/dist/viewers/structured-styles.d.ts +1 -1
  37. package/dist/viewers/structured-styles.js +1 -1
  38. package/dist/viewers/structured.d.ts +16 -0
  39. package/dist/viewers/structured.js +199 -48
  40. package/dist/viewers/toml/controller.d.ts +27 -1
  41. package/dist/viewers/toml/controller.js +64 -10
  42. package/dist/viewers/yaml/controller.js +48 -5
  43. package/package.json +10 -1
@@ -2,11 +2,14 @@ import { tomlPath } from './model.js';
2
2
  export function parseTomlText(text, limits = {}) {
3
3
  const settings = typeof limits === 'number' ? { maxDepth: limits } : limits;
4
4
  const maxDepth = settings.maxDepth ?? 100, maxNodes = settings.maxNodes ?? 100_000, maxTables = settings.maxTables ?? 50_000, maxArrayLength = settings.maxArrayLength ?? 100_000;
5
- const root = { kind: 'table', key: '$', path: '', raw: '', line: 1, children: [] };
5
+ const root = { kind: 'table', key: '$', path: '', raw: '', line: 1, range: { start: 0, end: text.length }, children: [] };
6
6
  const diagnostics = [];
7
7
  let current = root, nodeCount = 1, tableCount = 1, stopped = false;
8
8
  const tables = new Map([['', root]]);
9
- const lines = logicalTomlLines(text.replace(/^\uFEFF/, ''));
9
+ // Offsets stay relative to the untouched `text` so ranges address the same
10
+ // string a host renders. A leading BOM is dropped by `trim()` instead.
11
+ const lines = logicalTomlLines(text);
12
+ const lineAt = lineIndexer(text);
10
13
  const error = (code, line) => diagnostics.push({ severity: 'error', code, messageKey: `diag.${code}`, location: `line:${line}` });
11
14
  const unquote = (value) => {
12
15
  if (value.startsWith('"""') && value.endsWith('"""')) {
@@ -34,11 +37,15 @@ export function parseTomlText(text, limits = {}) {
34
37
  return value.slice(1, -1);
35
38
  return value;
36
39
  };
37
- const split = (value) => {
40
+ /** Split on top-level commas, reporting where each trimmed part starts so
41
+ * inline-table and array members can carry their own range. */
42
+ const split = (value, base) => {
38
43
  const out = [];
39
44
  let start = 0;
40
45
  let quote = '';
41
46
  let depth = 0;
47
+ const push = (from, to) => { const part = value.slice(from, to); const lead = part.length - part.trimStart().length; const trimmed = part.trim(); if (trimmed)
48
+ out.push({ text: trimmed, at: base + from + lead }); };
42
49
  for (let i = 0; i < value.length; i++) {
43
50
  const c = value[i];
44
51
  if (quote) {
@@ -52,16 +59,18 @@ export function parseTomlText(text, limits = {}) {
52
59
  else if (c === ']' || c === '}')
53
60
  depth--;
54
61
  else if (c === ',' && depth === 0) {
55
- out.push(value.slice(start, i).trim());
62
+ push(start, i);
56
63
  start = i + 1;
57
64
  }
58
65
  }
59
- out.push(value.slice(start).trim());
60
- return out.filter(Boolean);
66
+ push(start, value.length);
67
+ return out;
61
68
  };
62
69
  const keyParts = (key) => splitKey(key).map(unquote);
63
- const makeValue = (key, path, rawValue, line, depth) => {
70
+ const makeValue = (key, path, rawValue, line, depth, start) => {
64
71
  const raw = rawValue.trim();
72
+ const from = start + (rawValue.length - rawValue.trimStart().length);
73
+ const range = { start: from, end: from + raw.length };
65
74
  let kind = 'string';
66
75
  let value = unquote(raw);
67
76
  if (/^(true|false)$/.test(raw)) {
@@ -76,34 +85,37 @@ export function parseTomlText(text, limits = {}) {
76
85
  kind = 'datetime';
77
86
  else if (raw.startsWith('[') && raw.endsWith(']')) {
78
87
  kind = 'array';
79
- const values = split(raw.slice(1, -1));
88
+ const values = split(raw.slice(1, -1), from + 1);
80
89
  if (values.length > maxArrayLength) {
81
90
  error('toml.array-limit', line);
82
91
  values.length = maxArrayLength;
83
92
  }
84
- const children = values.map((v, i) => makeValue(String(i), `${path}[${i}]`, v, line, depth + 1));
93
+ const children = values.map((entry, i) => makeValue(String(i), `${path}[${i}]`, entry.text, lineAt(entry.at), depth + 1, entry.at));
85
94
  nodeCount += children.length;
86
- return { kind, key, path, raw, line, children };
95
+ return { kind, key, path, raw, line, range, children };
87
96
  }
88
97
  else if (raw.startsWith('{') && raw.endsWith('}')) {
89
98
  kind = 'table';
90
99
  const children = [];
91
- for (const pair of split(raw.slice(1, -1))) {
92
- const eq = indexOfEquals(pair);
100
+ for (const pair of split(raw.slice(1, -1), from + 1)) {
101
+ const eq = indexOfEquals(pair.text);
93
102
  if (eq < 0) {
94
- error('toml.invalid-inline-table', line);
103
+ error('toml.invalid-inline-table', lineAt(pair.at));
95
104
  continue;
96
105
  }
97
- const childKey = unquote(pair.slice(0, eq).trim());
98
- children.push(makeValue(childKey, tomlPath(path, childKey), pair.slice(eq + 1), line, depth + 1));
106
+ const childKey = unquote(pair.text.slice(0, eq).trim());
107
+ const child = makeValue(childKey, tomlPath(path, childKey), pair.text.slice(eq + 1), lineAt(pair.at), depth + 1, pair.at + eq + 1);
108
+ child.range = { start: pair.at, end: pair.at + pair.text.length };
109
+ children.push(child);
99
110
  }
100
- return { kind, key, path, raw, line, children };
111
+ return { kind, key, path, raw, line, range, children };
101
112
  }
102
113
  if (depth > maxDepth)
103
114
  error('toml.max-depth', line);
104
115
  nodeCount++;
105
- return { kind, key, path, value, raw, line };
116
+ return { kind, key, path, value, raw, line, range };
106
117
  };
118
+ let pending = [];
107
119
  for (const logical of lines) {
108
120
  if (settings.signal?.aborted) {
109
121
  error('aborted', logical.line);
@@ -116,9 +128,22 @@ export function parseTomlText(text, limits = {}) {
116
128
  break;
117
129
  }
118
130
  const i = logical.line - 1;
119
- const source = stripComment(logical.text).trim();
120
- if (!source)
131
+ const uncommented = stripComment(logical.text);
132
+ const trailing = commentBody(logical.text.slice(uncommented.length));
133
+ const source = uncommented.trim();
134
+ // Comment runs stay attached to the next declaration; a blank line ends
135
+ // the run so a file-header comment never leaks onto unrelated keys.
136
+ if (!source) {
137
+ if (trailing === undefined)
138
+ pending = [];
139
+ else
140
+ pending.push(trailing);
121
141
  continue;
142
+ }
143
+ const comment = [...pending, ...(trailing === undefined ? [] : [trailing])].join('\n') || undefined;
144
+ pending = [];
145
+ const base = logical.offset + (logical.text.length - logical.text.trimStart().length);
146
+ const declaration = { start: base, end: base + source.length };
122
147
  const arrayTable = source.match(/^\[\[(.+)\]\]$/);
123
148
  const table = source.match(/^\[(.+)\]$/);
124
149
  if (arrayTable || table) {
@@ -134,12 +159,12 @@ export function parseTomlText(text, limits = {}) {
134
159
  const parent = tables.get(parentPath) ?? root;
135
160
  let collection = parent.children.find(node => node.key === parts.at(-1) && node.kind === 'array');
136
161
  if (!collection) {
137
- collection = { kind: 'array', key: parts.at(-1), path, raw: '', line: i + 1, children: [] };
162
+ collection = { kind: 'array', key: parts.at(-1), path, raw: '', line: i + 1, range: declaration, children: [] };
138
163
  parent.children.push(collection);
139
164
  nodeCount++;
140
165
  }
141
166
  const index = collection.children.length;
142
- const node = { kind: 'table', key: String(index), path: `${path}[${index}]`, raw: '', line: i + 1, children: [] };
167
+ const node = { kind: 'table', key: String(index), path: `${path}[${index}]`, raw: '', line: i + 1, range: declaration, ...(comment ? { comment } : {}), children: [] };
143
168
  collection.children.push(node);
144
169
  tables.set(path, node);
145
170
  current = node;
@@ -149,7 +174,7 @@ export function parseTomlText(text, limits = {}) {
149
174
  else {
150
175
  let node = tables.get(path);
151
176
  if (!node) {
152
- node = { kind: 'table', key: parts.at(-1), path, raw: '', line: i + 1, children: [] };
177
+ node = { kind: 'table', key: parts.at(-1), path, raw: '', line: i + 1, range: declaration, ...(comment ? { comment } : {}), children: [] };
153
178
  (tables.get(parts.slice(0, -1).join('.')) ?? root).children.push(node);
154
179
  tables.set(path, node);
155
180
  nodeCount++;
@@ -175,7 +200,7 @@ export function parseTomlText(text, limits = {}) {
175
200
  for (const part of parts.slice(0, -1)) {
176
201
  let nested = parent.children.find(n => n.key === part && n.kind === 'table');
177
202
  if (!nested) {
178
- nested = { kind: 'table', key: part, path: tomlPath(parent.path, part), raw: '', line: i + 1, children: [] };
203
+ nested = { kind: 'table', key: part, path: tomlPath(parent.path, part), raw: '', line: i + 1, range: declaration, children: [] };
179
204
  parent.children.push(nested);
180
205
  }
181
206
  parent = nested;
@@ -183,25 +208,45 @@ export function parseTomlText(text, limits = {}) {
183
208
  const key = parts.at(-1);
184
209
  if (parent.children.some(n => n.key === key))
185
210
  error('toml.duplicate-key', i + 1);
186
- else
187
- parent.children.push(makeValue(key, tomlPath(parent.path, key), source.slice(eq + 1), i + 1, 0));
211
+ else {
212
+ const node = makeValue(key, tomlPath(parent.path, key), source.slice(eq + 1), i + 1, 0, base + eq + 1);
213
+ // The declaration reads better than the bare value when a host turns
214
+ // the range into a selection, so widen it to cover `key = value`.
215
+ node.range = declaration;
216
+ if (comment)
217
+ node.comment = comment;
218
+ parent.children.push(node);
219
+ }
188
220
  }
189
221
  return { document: { text, root }, diagnostics };
190
222
  }
191
223
  /** Group physical lines while a multiline string or a composite array/table
192
224
  * remains open. This preserves the declaration line for diagnostics. */
193
225
  function logicalTomlLines(text) {
194
- const physical = text.split(/\r\n|\n|\r/);
226
+ const physical = [];
227
+ {
228
+ const separator = /\r\n|\n|\r/g;
229
+ let last = 0;
230
+ let match;
231
+ while ((match = separator.exec(text))) {
232
+ physical.push({ text: text.slice(last, match.index), offset: last });
233
+ last = match.index + match[0].length;
234
+ }
235
+ physical.push({ text: text.slice(last), offset: last });
236
+ }
195
237
  const out = [];
196
- let buffer = '';
238
+ let open = false;
197
239
  let start = 1;
240
+ let offset = 0;
198
241
  let triple = '';
199
242
  let depth = 0;
200
243
  for (let i = 0; i < physical.length; i++) {
201
- const line = physical[i] ?? '';
202
- if (!buffer)
244
+ const { text: line, offset: at } = physical[i];
245
+ if (!open) {
246
+ open = true;
203
247
  start = i + 1;
204
- buffer += (buffer ? '\n' : '') + line;
248
+ offset = at;
249
+ }
205
250
  for (let p = 0; p < line.length; p++) {
206
251
  if (line.startsWith('"""', p) || line.startsWith("'''", p)) {
207
252
  const token = line.slice(p, p + 3);
@@ -219,16 +264,41 @@ function logicalTomlLines(text) {
219
264
  else if (c === ']' || c === '}')
220
265
  depth--;
221
266
  }
267
+ // Slicing the original text (rather than re-joining) keeps every offset
268
+ // inside a logical line usable as an index into `text`.
222
269
  if (!triple && depth <= 0) {
223
- out.push({ line: start, text: buffer });
224
- buffer = '';
270
+ out.push({ line: start, text: text.slice(offset, at + line.length), offset });
271
+ open = false;
225
272
  depth = 0;
226
273
  }
227
274
  }
228
- if (buffer)
229
- out.push({ line: start, text: buffer });
275
+ if (open)
276
+ out.push({ line: start, text: text.slice(offset), offset });
230
277
  return out;
231
278
  }
279
+ /** 1-based line lookup for offsets, used by inline members that may sit on a
280
+ * different physical line than their declaration. */
281
+ function lineIndexer(text) {
282
+ const starts = [0];
283
+ for (let i = 0; i < text.length; i++) {
284
+ const c = text[i];
285
+ if (c === '\n')
286
+ starts.push(i + 1);
287
+ else if (c === '\r') {
288
+ if (text[i + 1] === '\n')
289
+ i++;
290
+ starts.push(i + 1);
291
+ }
292
+ }
293
+ return offset => { let low = 0, high = starts.length - 1; while (low < high) {
294
+ const mid = (low + high + 1) >> 1;
295
+ if (starts[mid] <= offset)
296
+ low = mid;
297
+ else
298
+ high = mid - 1;
299
+ } return low + 1; };
300
+ }
301
+ function commentBody(rest) { const text = rest.trimStart(); return text.startsWith('#') ? text.slice(1).trim() : undefined; }
232
302
  function stripComment(line) { let q = ''; for (let i = 0; i < line.length; i++) {
233
303
  const c = line[i];
234
304
  if (q) {
@@ -18,6 +18,7 @@ export function parseYaml(input, options = {}) {
18
18
  const ast = options.deps.parse(text);
19
19
  const maxDocs = options.limits?.maxDocuments ?? 100;
20
20
  const docs = [];
21
+ const duplicates = [];
21
22
  for (let i = 0; i < Math.min(ast.length, maxDocs); i++) {
22
23
  if (options.signal?.aborted)
23
24
  return finish(docs.length ? { status: 'partial', document: { text, documents: docs }, diagnostics: [...diagnostics, { severity: 'warning', code: 'yaml.aborted', messageKey: 'diag.aborted' }] } : { status: 'failed', failure: { code: 'aborted', retryable: true, messageKey: 'diag.aborted' }, diagnostics });
@@ -28,11 +29,13 @@ export function parseYaml(input, options = {}) {
28
29
  diagnostics.push({ severity: 'error', code: 'yaml.invalid', messageKey: 'diag.yaml.invalid', location: pos ? `line:${pos.line}:${pos.col}` : `document:${i + 1}` });
29
30
  }
30
31
  const node = options.deps.normalize ? options.deps.normalize(sourceDoc, i) : normalizeUnknown(sourceDoc, `$doc[${i}]`);
31
- const violation = validateTree(node, options.limits?.maxEntries ?? 100_000, options.limits?.maxDepth ?? 100, options.limits?.maxAliases ?? 10_000);
32
+ const violation = validateTree(node, options.limits?.maxEntries ?? 100_000, options.limits?.maxDepth ?? 100, options.limits?.maxAliases ?? 10_000, duplicates);
32
33
  if (violation)
33
34
  diagnostics.push({ severity: 'warning', code: violation, messageKey: `diag.${violation}`, location: `document:${i + 1}` });
34
35
  docs.push(node);
35
36
  }
37
+ for (const duplicate of duplicates)
38
+ diagnostics.push({ severity: 'warning', code: 'yaml.duplicate-key', messageKey: 'diag.yaml.duplicate-key', args: { key: duplicate.key }, location: duplicate.path });
36
39
  if (ast.length > maxDocs)
37
40
  diagnostics.push({ severity: 'warning', code: 'yaml.document-limit', messageKey: 'diag.yaml.document-limit', args: { count: maxDocs } });
38
41
  return finish(diagnostics.length ? { status: 'partial', document: { text, documents: docs }, diagnostics } : { status: 'ok', document: { text, documents: docs }, diagnostics });
@@ -44,7 +47,8 @@ export function parseYaml(input, options = {}) {
44
47
  function normalizeUnknown(value, path, key = '$') { if (Array.isArray(value))
45
48
  return { kind: 'seq', key, path, children: value.map((v, i) => normalizeUnknown(v, `${path}[${i}]`, String(i))) }; if (value !== null && typeof value === 'object')
46
49
  return { kind: 'map', key, path, children: Object.entries(value).map(([k, v]) => normalizeUnknown(v, `${path}.${k}`, k)) }; return { kind: 'scalar', key, path, value: value, raw: String(value) }; }
47
- function validateTree(root, maxNodes, maxDepth, maxAliases) { let nodes = 0, aliases = 0; const stack = [{ node: root, depth: 0 }]; while (stack.length) {
50
+ const MAX_DUPLICATE_KEY_DIAGNOSTICS = 20;
51
+ function validateTree(root, maxNodes, maxDepth, maxAliases, duplicates) { let nodes = 0, aliases = 0; const stack = [{ node: root, depth: 0 }]; while (stack.length) {
48
52
  const item = stack.pop();
49
53
  nodes++;
50
54
  if (nodes > maxNodes)
@@ -53,6 +57,14 @@ function validateTree(root, maxNodes, maxDepth, maxAliases) { let nodes = 0, ali
53
57
  return 'yaml.depth-limit';
54
58
  if (item.node.kind === 'alias' && ++aliases > maxAliases)
55
59
  return 'yaml.alias-limit';
60
+ if (item.node.kind === 'map' && item.node.children) {
61
+ const seen = new Set();
62
+ for (const child of item.node.children) {
63
+ if (seen.has(child.key) && duplicates.length < MAX_DUPLICATE_KEY_DIAGNOSTICS && !duplicates.some(d => d.path === item.node.path && d.key === child.key))
64
+ duplicates.push({ path: item.node.path, key: child.key });
65
+ seen.add(child.key);
66
+ }
67
+ }
56
68
  for (const child of item.node.children ?? [])
57
69
  stack.push({ node: child, depth: item.depth + 1 });
58
70
  } return null; }
@@ -1,19 +1,31 @@
1
1
  /** Dynamic optional-peer loader; base parser/viewer modules never import yaml. */
2
2
  export async function loadYamlParserDeps() {
3
3
  const yaml = await import('yaml');
4
- const normalize = (node, path, key = '$') => {
5
- const n = node;
6
- if (n.contents !== undefined)
7
- return normalize(n.contents, path, key);
8
- const common = { key, path, range: n.range, anchorId: n.anchor };
9
- if (n.type === 'ALIAS' || n.constructor?.name === 'Alias')
10
- return { ...common, kind: 'alias', aliasOf: String(n.source ?? '') };
11
- if (Array.isArray(n.items)) {
12
- const pairs = n.items.every(item => item && typeof item === 'object' && 'key' in item);
13
- return { ...common, kind: pairs ? 'map' : 'seq', children: n.items.map((item, i) => { const pair = item; const childKey = pairs ? String(pair.key?.value ?? pair.key?.source ?? i) : String(i); return normalize(pairs ? pair.value : item, pairs ? `${path}.${childKey}` : `${path}[${i}]`, childKey); }) };
14
- }
15
- const value = n.value ?? node;
16
- return { ...common, kind: 'scalar', value: value, raw: n.source === undefined ? String(value) : String(n.source) };
4
+ const normalizeDocument = (document, index) => {
5
+ const anchors = new Map();
6
+ const record = (node) => { if (node.anchorId)
7
+ anchors.set(node.anchorId, node); return node; };
8
+ const normalize = (node, path, key = '$') => {
9
+ const n = node;
10
+ if (n.contents !== undefined)
11
+ return normalize(n.contents, path, key);
12
+ const common = { key, path, range: n.range, anchorId: n.anchor };
13
+ if (n.type === 'ALIAS' || n.constructor?.name === 'Alias') {
14
+ const name = String(n.source ?? '');
15
+ const target = anchors.get(name);
16
+ if (target?.kind !== 'scalar')
17
+ return { ...common, kind: 'alias', aliasOf: name, raw: `*${name}` };
18
+ return { ...common, kind: 'alias', aliasOf: name, value: target.value ?? null, raw: target.raw ?? `*${name}` };
19
+ }
20
+ if (Array.isArray(n.items)) {
21
+ const pairs = n.items.every(item => item && typeof item === 'object' && 'key' in item);
22
+ const seen = new Map();
23
+ return record({ ...common, kind: pairs ? 'map' : 'seq', children: n.items.map((item, i) => { const pair = item; const keyValue = typeof pair.key?.value === 'symbol' ? pair.key?.source ?? '<<' : pair.key?.value; const childKey = pairs ? String(keyValue ?? pair.key?.source ?? i) : String(i); const occurrence = (seen.get(childKey) ?? 0) + 1; seen.set(childKey, occurrence); return normalize(pairs ? pair.value : item, pairs ? `${path}.${childKey}${occurrence > 1 ? `#${occurrence}` : ''}` : `${path}[${i}]`, childKey); }) });
24
+ }
25
+ const value = n.value ?? node;
26
+ return record({ ...common, kind: 'scalar', value: value, raw: n.source === undefined ? String(value) : String(n.source) });
27
+ };
28
+ return normalize(document, `$doc[${index}]`);
17
29
  };
18
- return { parse: (text) => yaml.parseAllDocuments(text, { schema: 'core', customTags: [] }), normalize: (document, index) => normalize(document, `$doc[${index}]`) };
30
+ return { parse: (text) => yaml.parseAllDocuments(text, { schema: 'core', customTags: [], uniqueKeys: false, merge: true }), normalize: normalizeDocument };
19
31
  }
@@ -61,6 +61,7 @@ export declare const REQIF_VIEWER_DESCRIPTOR: ViewerDescriptor;
61
61
  export declare const HDF5_MAGIC_PREFIX: readonly [137, 72, 68, 70, 13, 10, 26, 10];
62
62
  export declare const HDF5_VIEWER_DESCRIPTOR: ViewerDescriptor;
63
63
  export declare const MAT_VIEWER_DESCRIPTOR: ViewerDescriptor;
64
+ export declare const SAFETENSORS_VIEWER_DESCRIPTOR: ViewerDescriptor;
64
65
  export declare const AUDIO_VIEWER_DESCRIPTOR: ViewerDescriptor;
65
66
  export declare const VIDEO_VIEWER_DESCRIPTOR: ViewerDescriptor;
66
67
  export declare const DBC_VIEWER_DESCRIPTOR: ViewerDescriptor;
@@ -72,7 +72,7 @@ export const PDF_VIEWER_DESCRIPTOR = {
72
72
  priority: 20,
73
73
  magicPrefix: [0x25, 0x50, 0x44, 0x46, 0x2d], // '%PDF-'
74
74
  requiredServices: [],
75
- optionalServices: ['save', 'writeback', 'filePick']
75
+ optionalServices: ['save', 'writeback', 'filePick', 'clipboard']
76
76
  };
77
77
  export const PARQUET_MAGIC_SIGNATURES = [[{ offset: 0, bytes: [0x50, 0x41, 0x52, 0x31] }]];
78
78
  export const PARQUET_VIEWER_DESCRIPTOR = { id: 'parquet', displayNameKey: 'parquet.title', extensions: ['parquet'], priority: 20, magicSignatures: PARQUET_MAGIC_SIGNATURES, requiredServices: [], optionalServices: ['clipboard', 'save'] };
@@ -87,6 +87,9 @@ export const HDF5_MAGIC_PREFIX = [0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a
87
87
  // signature still enables extensionless content routing below.
88
88
  export const HDF5_VIEWER_DESCRIPTOR = { id: 'hdf5', displayNameKey: 'hdf5.title', extensions: ['h5', 'hdf5', 'he5'], priority: 20, requiredServices: [], optionalServices: ['clipboard'] };
89
89
  export const MAT_VIEWER_DESCRIPTOR = { id: 'mat', displayNameKey: 'mat.title', extensions: ['mat'], priority: 20, requiredServices: [], optionalServices: ['clipboard'] };
90
+ // Safetensors has no fixed leading magic (the file opens with an 8-byte header
91
+ // length), so it is admitted by extension and validated by the parser.
92
+ export const SAFETENSORS_VIEWER_DESCRIPTOR = { id: 'safetensors', displayNameKey: 'safetensors.title', extensions: ['safetensors'], priority: 20, requiredServices: [], optionalServices: ['clipboard'] };
90
93
  export const AUDIO_VIEWER_DESCRIPTOR = { id: 'audio', displayNameKey: 'audio.title', extensions: ['mp3', 'wav', 'pcm', 'aiff', 'aif', 'aifc', 'amr', 'awb', 'ogg', 'flac', 'ac3', 'aac', 'm4a'], priority: 20, requiredServices: [], optionalServices: [] };
91
94
  export const VIDEO_VIEWER_DESCRIPTOR = { id: 'video', displayNameKey: 'video.title', extensions: ['mp4', 'mts', 'm2ts', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'], priority: 20, requiredServices: [], optionalServices: [] };
92
95
  export const DBC_VIEWER_DESCRIPTOR = { id: 'dbc', displayNameKey: 'dbc.title', extensions: ['dbc'], priority: 20, requiredServices: [], optionalServices: ['clipboard'] };
@@ -162,7 +165,7 @@ export const IMAGE_VIEWER_DESCRIPTOR = {
162
165
  export const MARKDOWN_VIEWER_DESCRIPTOR = {
163
166
  id: 'markdown', displayNameKey: 'markdown.title',
164
167
  extensions: ['md', 'markdown', 'mdown', 'mkdn', 'mkd'], priority: 10,
165
- requiredServices: [], optionalServices: ['clipboard', 'navigation', 'documentAssets', 'writeback']
168
+ requiredServices: [], optionalServices: ['clipboard', 'navigation', 'documentAssets', 'writeback', 'save']
166
169
  };
167
170
  export const ARCHIVE_MAGIC_SIGNATURES = [
168
171
  [{ offset: 0, bytes: [0x50, 0x4b, 0x03, 0x04] }], [{ offset: 0, bytes: [0x50, 0x4b, 0x05, 0x06] }],
@@ -200,6 +203,7 @@ export const CORE_VIEWER_DESCRIPTORS = [
200
203
  REQIF_VIEWER_DESCRIPTOR,
201
204
  HDF5_VIEWER_DESCRIPTOR,
202
205
  MAT_VIEWER_DESCRIPTOR,
206
+ SAFETENSORS_VIEWER_DESCRIPTOR,
203
207
  PPT_VIEWER_DESCRIPTOR,
204
208
  WORD_VIEWER_DESCRIPTOR,
205
209
  HWP_VIEWER_DESCRIPTOR,
@@ -51,6 +51,14 @@
51
51
  color: var(--omni-fg-muted, #9d9d9d);
52
52
  margin-right: 8px;
53
53
  }
54
+ .omni-pdf__save-mode {
55
+ padding: 2px 6px;
56
+ color: var(--omni-fg-muted, #9d9d9d);
57
+ border: 1px solid var(--omni-border, #3c3c3c);
58
+ border-radius: 999px;
59
+ font-size: 11px;
60
+ white-space: nowrap;
61
+ }
54
62
  .omni-pdf__page-input {
55
63
  width: 52px;
56
64
  padding: 3px 5px;
@@ -175,6 +183,17 @@
175
183
  min-width: 56px;
176
184
  text-align: center;
177
185
  }
186
+ .omni-pdf__thumbs-toggle {
187
+ width: 30px;
188
+ height: 28px;
189
+ padding: 0;
190
+ margin-right: auto;
191
+ font-size: 16px;
192
+ line-height: 1;
193
+ }
194
+ .omni-pdf__thumbs-toggle.is-collapsed {
195
+ opacity: 0.6;
196
+ }
178
197
  .omni-pdf__view-menu-wrap { position: relative; }
179
198
  .omni-pdf__view-menu-btn {
180
199
  width: 30px;
@@ -219,12 +238,19 @@
219
238
  background: var(--omni-border, #3c3c3c);
220
239
  }
221
240
  .omni-pdf__body {
241
+ position: relative;
222
242
  flex: 1;
223
243
  min-height: 0;
224
244
  display: grid;
225
245
  grid-template-columns: 160px minmax(0, 1fr);
226
246
  overflow: hidden;
227
247
  }
248
+ .omni-pdf__body--thumbs-collapsed {
249
+ grid-template-columns: minmax(0, 1fr);
250
+ }
251
+ .omni-pdf__body--thumbs-collapsed .omni-pdf__thumbs {
252
+ display: none;
253
+ }
228
254
  .omni-pdf__thumbs {
229
255
  overflow: auto;
230
256
  min-height: 0;
@@ -265,10 +291,28 @@
265
291
  .omni-pdf__thumb.drop-before::before { top: -6px; }
266
292
  .omni-pdf__thumb.drop-after::after { bottom: -6px; }
267
293
  .omni-pdf__thumb canvas {
294
+ display: block;
268
295
  max-width: 100%;
269
296
  background: #ffffff;
270
297
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
271
298
  }
299
+ .omni-pdf__thumb-page {
300
+ position: relative;
301
+ max-width: 100%;
302
+ }
303
+ .omni-pdf__thumb-annotation {
304
+ position: absolute;
305
+ z-index: 1;
306
+ overflow: hidden;
307
+ pointer-events: none;
308
+ color: #111111; /* ink on paper */
309
+ white-space: nowrap;
310
+ }
311
+ .omni-pdf__thumb-annotation img { display: block; }
312
+ .omni-pdf__thumb-annotation.omni-pdf__underline::after,
313
+ .omni-pdf__thumb-annotation.omni-pdf__strikeout::after {
314
+ height: 1px;
315
+ }
272
316
  .omni-pdf__thumb-label {
273
317
  font-family: var(--omni-font-mono, Monaco, Menlo, monospace);
274
318
  font-size: 10px;
@@ -438,6 +482,161 @@
438
482
  content: '';
439
483
  background: var(--omni-hl-color, #ffeb3b);
440
484
  }
485
+ /* Floating action bar shown above a selected text-markup annotation. */
486
+ .omni-pdf__markup-toolbar {
487
+ position: absolute;
488
+ z-index: 5;
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 2px;
492
+ padding: 4px;
493
+ transform: translateY(calc(-100% - 8px));
494
+ background: var(--omni-bg-secondary, #252526);
495
+ border: 1px solid var(--omni-border, #3c3c3c);
496
+ border-radius: 10px;
497
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
498
+ cursor: default;
499
+ }
500
+ .omni-pdf button.omni-pdf__markup-tool {
501
+ display: inline-flex;
502
+ align-items: center;
503
+ justify-content: center;
504
+ width: 30px;
505
+ height: 30px;
506
+ padding: 0;
507
+ border: 0;
508
+ border-radius: 8px;
509
+ background: transparent;
510
+ color: var(--omni-fg, #e8e8e8);
511
+ font-size: 15px;
512
+ line-height: 1;
513
+ cursor: pointer;
514
+ }
515
+ .omni-pdf button.omni-pdf__markup-tool:hover:not(:disabled) { background: var(--omni-button-hover-bg, #45494e); }
516
+ .omni-pdf button.omni-pdf__markup-tool:disabled { opacity: 0.4; cursor: default; }
517
+ .omni-pdf__markup-color::before {
518
+ content: '';
519
+ width: 18px;
520
+ height: 18px;
521
+ border-radius: 5px;
522
+ background: var(--omni-hl-color, #ffeb3b);
523
+ border: 1px solid rgba(0, 0, 0, 0.25);
524
+ }
525
+ .omni-pdf__markup-color-wrap { position: relative; display: inline-flex; }
526
+ .omni-pdf__markup-palette {
527
+ position: absolute;
528
+ top: calc(100% + 6px);
529
+ left: 50%;
530
+ transform: translateX(-50%);
531
+ display: none;
532
+ grid-template-columns: repeat(3, auto);
533
+ gap: 6px;
534
+ padding: 8px;
535
+ background: var(--omni-bg-secondary, #252526);
536
+ border: 1px solid var(--omni-border, #3c3c3c);
537
+ border-radius: 8px;
538
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
539
+ z-index: 6;
540
+ }
541
+ .omni-pdf__markup-palette.is-open { display: grid; }
542
+ .omni-pdf button.omni-pdf__markup-swatch,
543
+ .omni-pdf button.omni-pdf__markup-swatch:hover:not(:disabled) {
544
+ width: 22px;
545
+ height: 22px;
546
+ padding: 0;
547
+ border-radius: 5px;
548
+ border: 1px solid rgba(255, 255, 255, 0.25);
549
+ background: var(--omni-hl-color, #ffeb3b);
550
+ cursor: pointer;
551
+ }
552
+ .omni-pdf button.omni-pdf__markup-swatch.is-current {
553
+ outline: 2px solid var(--omni-accent, #0e639c);
554
+ outline-offset: 1px;
555
+ }
556
+ /* Left overlay panel listing every text-markup annotation. */
557
+ .omni-pdf__markup-list {
558
+ position: absolute;
559
+ top: 0;
560
+ left: 0;
561
+ bottom: 0;
562
+ z-index: 6;
563
+ display: none;
564
+ flex-direction: column;
565
+ width: min(280px, 70%);
566
+ background: var(--omni-bg-secondary, #252526);
567
+ border-right: 1px solid var(--omni-border, #3c3c3c);
568
+ box-shadow: 2px 0 12px rgba(0, 0, 0, 0.35);
569
+ overflow-y: auto;
570
+ }
571
+ .omni-pdf__markup-list.is-open { display: flex; }
572
+ .omni-pdf__markup-list-head {
573
+ position: sticky;
574
+ top: 0;
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: space-between;
578
+ padding: 10px 12px;
579
+ background: var(--omni-bg-secondary, #252526);
580
+ border-bottom: 1px solid var(--omni-border, #3c3c3c);
581
+ }
582
+ .omni-pdf__markup-list-title { font-weight: 600; }
583
+ .omni-pdf button.omni-pdf__markup-list-close {
584
+ width: 26px;
585
+ height: 26px;
586
+ padding: 0;
587
+ border: 0;
588
+ border-radius: 6px;
589
+ background: transparent;
590
+ color: var(--omni-fg, #e8e8e8);
591
+ font-size: 18px;
592
+ line-height: 1;
593
+ cursor: pointer;
594
+ }
595
+ .omni-pdf button.omni-pdf__markup-list-close:hover:not(:disabled) { background: var(--omni-button-hover-bg, #45494e); }
596
+ .omni-pdf__markup-list-empty {
597
+ padding: 16px 12px;
598
+ color: var(--omni-fg-muted, #9d9d9d);
599
+ font-size: 13px;
600
+ }
601
+ .omni-pdf button.omni-pdf__markup-item {
602
+ display: flex;
603
+ align-items: center;
604
+ gap: 8px;
605
+ width: 100%;
606
+ padding: 10px 12px;
607
+ border: 0;
608
+ border-bottom: 1px solid var(--omni-border, #3c3c3c);
609
+ background: transparent;
610
+ color: var(--omni-fg, #e8e8e8);
611
+ text-align: left;
612
+ cursor: pointer;
613
+ }
614
+ .omni-pdf button.omni-pdf__markup-item:hover:not(:disabled) { background: var(--omni-button-hover-bg, #45494e); }
615
+ .omni-pdf button.omni-pdf__markup-item.is-selected {
616
+ background: var(--omni-button-hover-bg, #45494e);
617
+ box-shadow: inset 3px 0 0 var(--omni-accent, #0e639c);
618
+ }
619
+ .omni-pdf__markup-item-dot {
620
+ flex: none;
621
+ width: 14px;
622
+ height: 14px;
623
+ border-radius: 3px;
624
+ border: 1px solid rgba(255, 255, 255, 0.25);
625
+ background: var(--omni-hl-color, #ffeb3b);
626
+ }
627
+ .omni-pdf__markup-item-text {
628
+ flex: 1;
629
+ min-width: 0;
630
+ overflow: hidden;
631
+ text-overflow: ellipsis;
632
+ white-space: nowrap;
633
+ font-size: 13px;
634
+ }
635
+ .omni-pdf__markup-item-page {
636
+ flex: none;
637
+ color: var(--omni-fg-muted, #9d9d9d);
638
+ font-size: 12px;
639
+ }
441
640
  .omni-pdf__text-editor {
442
641
  position: absolute;
443
642
  z-index: 3;
@@ -543,9 +742,13 @@
543
742
  grid-template-columns: minmax(0, 1fr);
544
743
  grid-template-rows: minmax(120px, 30vh) minmax(0, 1fr);
545
744
  }
745
+ .omni-pdf__body--thumbs-collapsed {
746
+ grid-template-rows: minmax(0, 1fr);
747
+ }
546
748
  .omni-pdf__thumbs {
547
749
  border-right: 0;
548
750
  border-bottom: 1px solid var(--omni-border, #3c3c3c);
549
751
  }
550
752
  .omni-pdf__pages { grid-row: 2; }
753
+ .omni-pdf__body--thumbs-collapsed .omni-pdf__pages { grid-row: 1; }
551
754
  }