arcane-os 0.45.0 → 0.46.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.46.0
4
+
5
+ - Add public `decodeMarkdownMediaRecord` for complete stored image records or
6
+ JSON text without storage or network access. Preserve singleton record layers,
7
+ original content and explicit decoding errors; never select from multiple
8
+ records. File previews recognize declared image records in JSON-family files
9
+ while ordinary untyped/non-image JSON and application preview hooks keep their
10
+ existing presentation and precedence.
11
+ - Preserve complete standalone IMG tags with multiline quoted descriptions
12
+ through the shared Markdown renderer and its original-source mapping. Native
13
+ HTML handling and literal Markdown examples keep their existing parsing;
14
+ stored Markdown is unchanged and requires no migration.
15
+ - Use the existing light paper palette and 15mm page margins for rendered print
16
+ snapshots and direct shared-theme printing. Preserve full content, rendered
17
+ fonts, image pixels, SVG artwork, live screen preferences, cancellation and
18
+ resource ownership through afterprint. A dialog request is not proof of
19
+ physical printing or PDF completion.
20
+ - Keep image destinations, export selection, model and credentials application
21
+ owned. No saved-data migration, model/provider/default change or app layout
22
+ change is introduced.
23
+
3
24
  ## 0.45.0
4
25
 
5
26
  - Add public MarkdownMedia save, read, reference parsing and hydration helpers.
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.45.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.46.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -54,6 +54,16 @@ imported component's `destroy()` through `HTMLImport`; callers that invoke
54
54
  `destroy()` directly retain responsibility for removing the element when
55
55
  appropriate.
56
56
 
57
+ ## Shared print presentation
58
+
59
+ Applications loading the shared `theme.css` receive a print-only base light
60
+ palette with a white paper surface and `15mm` page margins. Root, body, and
61
+ scoped `data-arcane-palette` tokens override screen/custom colors during print;
62
+ root/body margins are reset. Saved preferences, screen presentation, scrolling,
63
+ media, and other layout rules remain unchanged. Registered rendered-print
64
+ views use the separate [`PrintView.js`](runtime-modules.md#printviewjs) snapshot
65
+ owner described with the editor and file preview below.
66
+
57
67
  ## Canonical inventory
58
68
 
59
69
  | Component | Capability | Principal methods | Events | Normalization |
@@ -919,7 +929,16 @@ extension. Routing compares the lowercase MIME essence without its parameters;
919
929
  the callback's `mimeType` and the file content remain unchanged.
920
930
  HTML uses its complete original string in `srcdoc` without an
921
931
  added sandbox policy; Markdown reads the complete original text before
922
- rendering. Untransformed JSONL/NDJSON shows its complete original text,
932
+ rendering. With no descriptor or `previewTransform`, JSON, JSONL, and NDJSON
933
+ files are read in full and recognized through
934
+ [`decodeMarkdownMediaRecord()`](runtime-modules.md#markdownmediajs). A complete
935
+ stored image record, including nested single-record arrays, opens as an image
936
+ when its decoded Blob has an `image/` media type. Untyped/non-image records,
937
+ multiple records, and unrecognized content keep their ordinary JSON-family
938
+ presentation; malformed recognized image encoding follows the existing
939
+ file-open failure path.
940
+ This recognition leaves saved content unchanged and performs no migration.
941
+ Untransformed JSONL/NDJSON shows its complete original text,
923
942
  including lines that are not JSON records. Other binary files offer a download
924
943
  of the original `File`.
925
944
  The larger file-preview dialog reuses `--modal-width`, `--modal-max-height`,
@@ -931,6 +950,8 @@ preview ahead of the editor. Printing uses the shared
931
950
  [`PrintView.js`](runtime-modules.md#printviewjs) owner; accessible HTML frames
932
951
  are copied from their current rendered document, never substituted with source
933
952
  text. Explicit printing waits for images/fonts and HTML frame readiness.
953
+ The shared print surface uses a light paper palette and `15mm` page margins
954
+ while preserving image/SVG colors and leaving the screen theme unchanged.
934
955
  Native PDF and inaccessible embedded viewers require their own print command.
935
956
  `true` means the browser print dialog was requested, not that paper or a PDF
936
957
  was produced. Closed, destroyed, or unavailable previews return `false`;
@@ -945,6 +966,9 @@ pending media because it cannot await them. Closing or replacing a preview
945
966
  cancels display readiness without waiting for storage; URLs already retained
946
967
  by a requested print remain until `afterprint`. Original Markdown and ordinary
947
968
  external image URLs remain unchanged.
969
+ Standalone IMG blocks with multiline quoted descriptions use the shared
970
+ [`MD.js`](runtime-modules.md#mdjs) tokenizer in both ordinary and source-mapped
971
+ rendering; the complete description and stored Markdown remain intact.
948
972
 
949
973
  `await close()` cancels pending presentation work and closes delete, file, and
950
974
  directory dialogs in that order. A settled preview keeps its rendered content,
@@ -957,7 +981,7 @@ writes complete through the storage owner without reopening closed dialogs.
957
981
 
958
982
  Events: `file-manager-ready`, `file-manager-select`, `file-manager-open`, `file-manager-action`.
959
983
 
960
- Shared dependencies: [`DBOPFS.js`](runtime-modules.md#dbopfsjs), [`File.js`](runtime-entities.md#filejs), [`MD.js`](runtime-modules.md#mdjs), [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
984
+ Shared dependencies: [`DBOPFS.js`](runtime-modules.md#dbopfsjs), [`File.js`](runtime-entities.md#filejs), [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs), [`PrintView.js`](runtime-modules.md#printviewjs), [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
961
985
 
962
986
  ### Availability and normalization
963
987
 
@@ -1082,6 +1106,9 @@ diagnostics remain in the developer console; successful images and document
1082
1106
  text remain visible. Replacing, clearing, failing, or destroying the document
1083
1107
  releases its display resources without deleting stored media. Markdown source
1084
1108
  and ordinary external image URLs remain unchanged.
1109
+ Complete standalone IMG blocks with quoted multiline descriptions render
1110
+ through the shared [`MD.js`](runtime-modules.md#mdjs) tokenizer without
1111
+ rewriting saved Markdown.
1085
1112
 
1086
1113
  Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs).
1087
1114
 
@@ -1125,8 +1152,10 @@ The visible **Print** control and `await print()` print the complete rendered
1125
1152
  Markdown with `entryTitle`, independently of saving. Native browser Print uses
1126
1153
  that same registered view unless an open file preview takes precedence.
1127
1154
  The SDK copies current rendered DOM/styles into a temporary print-only view,
1128
- leaving the editor's source, controls, and screen layout in place. Explicit
1129
- printing waits for resource readiness; native `beforeprint` is synchronous
1155
+ leaving the editor's source, controls, and screen layout in place. Both explicit
1156
+ and native print use the shared light paper palette and `15mm` page margins;
1157
+ image pixels and SVG artwork keep their colors. Explicit printing waits for
1158
+ resource readiness; native `beforeprint` is synchronous
1130
1159
  and uses the currently rendered content. `print()` returns `false` when the
1131
1160
  view is unavailable, rejects preparation failures, and returns `true` only
1132
1161
  when the browser dialog was requested. Completion of physical printing or PDF
@@ -1142,6 +1171,9 @@ waits for the current image reads/decoding, follows a replacement preview, and
1142
1171
  rechecks readiness before capture. Native Print reports still-pending media
1143
1172
  synchronously; it cannot wait or cancel the native dialog. Print-owned URLs
1144
1173
  outlive a changed or destroyed editor until `afterprint`.
1174
+ The shared [`MD.js`](runtime-modules.md#mdjs) tokenizer preserves complete
1175
+ standalone IMG blocks with quoted multiline descriptions in ordinary and
1176
+ source-mapped rendering. Existing saved markup needs no rewriting.
1145
1177
 
1146
1178
  `configure({fit:true,followPreview:true})` opts into the two independent
1147
1179
  behaviors. Both options default to `false`; omitted options retain their
@@ -96,7 +96,7 @@ own asynchronous work, cancellation, and backpressure.
96
96
  | [`Mail.js`](#mailjs) | esm | Builds complete reports and prefers the native mail capability with an explicit HTTP transport fallback. | Browser/native hybrid + cloud | Mail inputs/results normalized; transport failures mixed. |
97
97
  | [`MailOutbox.mjs`](#mailoutboxmjs) | esm | Persists complete mail reports before delivery and normalizes idempotent enqueue, retry, reconciliation, and invalid-record maintenance. | Browser/native WebView or compatible injected host | Complete records, full work, cancellation, and lifecycle states normalized; storage, lock, and delivery failures coded. |
98
98
  | [`MailTransport.mjs`](#mailtransportmjs) | esm | Sends one complete mail report to a normalized HTTP(S) endpoint. | Browser/server with fetch + cloud | Normalized endpoint and transport errors; remote detail preserved. |
99
- | [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately and resolves their stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
99
+ | [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately, decodes complete stored records, and resolves stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
100
100
  | [`MarkdownSpeech.js`](#markdownspeechjs) | esm | Removes repeated Markdown formatting marks from streamed narration. | Cross-host | Speech-only filtering; single marks and ordinary punctuation remain literal. |
101
101
  | [`Marked.min.js`](#markedminjs) | esm | Vendored Marked 18.0.5 Markdown lexer, parser, renderer, extension, and walk-token API. | Cross-host vendor module | Vendor-native Marked contract. |
102
102
  | [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors. | Browser / native WebView | Complete raw and rendered Marked values; opt-in original-source offsets; parse errors vendor-native. |
@@ -2816,9 +2816,18 @@ Public package import: `arcane-os/modules/MarkdownMedia.js`.
2816
2816
  - `parseMarkdownMediaReference(reference)` returns `{tableName,fileName}` for
2817
2817
  a local reference, or `null` for an ordinary URL. Malformed local addresses
2818
2818
  report their actual parsing error.
2819
+ - `decodeMarkdownMediaRecord(value)` synchronously decodes a complete
2820
+ `{dataUrl,mediaType?}` record or its JSON text into a `Blob`, without storage
2821
+ or network access. Nested single-record arrays are unwrapped; arrays with
2822
+ multiple records are never reduced to one image. Unrecognized content,
2823
+ including text that is not complete JSON, returns `null`. A recognized
2824
+ record with unreadable base64 data-URL content throws its decoding error.
2825
+ Omitted `mediaType` preserves the Blob's empty type. Input remains unchanged.
2819
2826
  - `await readMarkdownMedia(reference)` returns the complete `Blob`, using
2820
2827
  only local storage and data-URL decoding; stored strings are never fetched
2821
- as network addresses.
2828
+ as network addresses. It uses the same record decoder after loading, while
2829
+ preserving JSON parse errors for unreadable stored JSON strings and reporting
2830
+ an unreadable record as an error rather than `null`.
2822
2831
  - `hydrateMarkdownMedia(root,{signal}={})` returns `{ready,destroy,retain}`
2823
2832
  immediately. It includes an IMG root and descendant IMG elements, removes
2824
2833
  local-reference `src` values synchronously, starts independent reads
@@ -2841,6 +2850,9 @@ records their existing JSON backup/export includes. Reusing the same filename
2841
2850
  supports an application-selected save-only retry without another generation
2842
2851
  request. The helper performs no existing-data migration.
2843
2852
 
2853
+ Exact exports: `saveMarkdownMedia`, `parseMarkdownMediaReference`,
2854
+ `decodeMarkdownMediaRecord`, `readMarkdownMedia`, `hydrateMarkdownMedia`.
2855
+
2844
2856
  ### Availability and normalization
2845
2857
 
2846
2858
  Browser or native WebView with the existing app-scoped DBOPFS owner, Blob,
@@ -2896,16 +2908,29 @@ mapping, not exact rendered glyph or caret geometry.
2896
2908
 
2897
2909
  Assigning `raw` or calling `append(string)` rebuilds both the complete rendered
2898
2910
  result and its current mapping. Without `sourceMap:true`, `sourceMap` is empty
2899
- and the ordinary rendered output is unchanged. Existing raw content, link
2911
+ and the same parsing runs without comment anchors. Existing raw content, link
2900
2912
  behavior, no-op `rendered` setter, and complete `append()` result remain.
2901
2913
 
2914
+ Both rendering paths first use Marked's native HTML block tokenizer. When it
2915
+ does not recognize a complete standalone `<img>` tag with a newline inside a
2916
+ quoted attribute value, MD keeps that complete tag together as one HTML block.
2917
+ This includes blank lines in an image's full `alt` description. The tag must
2918
+ start at an existing Markdown block boundary with at most three leading spaces
2919
+ and end on its own line or at the end of the source. Separate inserted image
2920
+ blocks from surrounding Markdown with blank lines. Native HTML handling,
2921
+ inline prose, fenced/indented/inline code, escaped examples, and incomplete
2922
+ tags retain their existing parsing behavior. No source is rewritten or saved
2923
+ data migrated. Marked's existing rendered line-ending normalization remains;
2924
+ mapped offsets still address the exact original source, including CRLF or CR.
2925
+
2902
2926
  Exact exports: `default`.
2903
2927
 
2904
2928
  ### Availability and normalization
2905
2929
 
2906
- **Browser / native WebView.** Raw Marked behavior is preserved; parse errors are
2907
- vendor-native. Rendering and the getters are DOM-independent. DOM lookup and
2908
- scrolling belong to the consuming component;
2930
+ **Browser / native WebView.** Marked parsing, including the shared standalone
2931
+ image-token correction above; parse errors remain vendor-native. Rendering and
2932
+ the getters are DOM-independent. DOM lookup and scrolling belong to the
2933
+ consuming component;
2909
2934
  mapping performs no persistence or viewport movement. Transport: Marked.
2910
2935
  [Deep protocol details](protocols.md).
2911
2936
 
@@ -3345,6 +3370,13 @@ Shared rendered-print owner used by Markdown Editor and File Manager. It
3345
3370
  captures the current DOM and computed styles, including accessible rendered
3346
3371
  HTML frames, complete text, selected images, and the current title. A temporary
3347
3372
  print-only surface expands scrollable content without editing the live screen.
3373
+ That surface uses the shared Arcane light-theme text, paper, and border colors,
3374
+ with `15mm` page margins. It retains rendered fonts, emphasis, and complete
3375
+ content while removing screen-theme text/shadow paint from ordinary HTML and
3376
+ pseudo-elements. Links are underlined and table cells receive visible borders.
3377
+ Image pixels, canvas snapshots, and SVG artwork keep their own colors. These
3378
+ print rules leave the live screen theme unchanged; browser print settings still
3379
+ control the eventual page output.
3348
3380
 
3349
3381
  ### Public surface
3350
3382
 
@@ -4022,12 +4054,20 @@ is applied once the body is available.
4022
4054
 
4023
4055
  The shared `theme.css` owns the default, warm, curious, hopeful, harmony, and
4024
4056
  warrior palettes. Each works with explicit light/dark or system appearance.
4025
- `layout.css` imports that same owner. Explicit custom themes retain precedence
4026
- through `data-arcane-skin="custom"` and their inline tokens.
4057
+ `layout.css` imports that same owner. On screen, explicit custom themes retain
4058
+ precedence through `data-arcane-skin="custom"` and their inline tokens.
4027
4059
  The bare root retains the generic neutral light/dark baseline; the prior
4028
4060
  layout default palette is selected explicitly through `data-user-skin="default"`,
4029
4061
  `body.default` compatibility, or the default swatch.
4030
4062
 
4063
+ During printing, `theme.css` applies the existing base light palette, with a
4064
+ white paper surface and `15mm` page margins, to the root, body, and scoped
4065
+ `data-arcane-palette` elements. These print-only tokens override screen/custom
4066
+ palette tokens. It resets the root/body margin without changing saved
4067
+ preferences, screen presentation, scrolling, media, or other layout rules.
4068
+ The separate [`PrintView.js`](#printviewjs) owner uses the same light palette
4069
+ and margins for its expanded rendered-content snapshot.
4070
+
4031
4071
  A scoped `data-arcane-palette="warm"` element exposes the same palette variables
4032
4072
  for a chooser preview without changing the page's selection. Use any of the
4033
4073
  six named palettes and paint the preview with ordinary shared variables such
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
92
92
  "check": "node tools/check-source.mjs && npm test",
@@ -2142,8 +2142,9 @@
2142
2142
  const rendered=document.createElement('div');
2143
2143
  rendered.className='file-preview';
2144
2144
  let value=fileRef.parsed;
2145
- const transformed=['json','jsonl','ndjson'].includes(fileRef.ext)
2146
- &&is.function(host.previewTransform);
2145
+ const jsonFile=['json','jsonl','ndjson'].includes(fileRef.ext);
2146
+ const transformed=jsonFile&&is.function(host.previewTransform);
2147
+ let completeJSON;
2147
2148
 
2148
2149
  if(transformed){
2149
2150
  value=await host.previewTransform(
@@ -2155,6 +2156,37 @@
2155
2156
  }
2156
2157
  );
2157
2158
  preview.controller.signal.throwIfAborted();
2159
+ }else if(jsonFile){
2160
+ // Recognition needs the complete file: FileEntity's parsed JSONL
2161
+ // rows do not include every unparseable line in the original.
2162
+ const [{decodeMarkdownMediaRecord},text]=await Promise.all([
2163
+ import('../modules/MarkdownMedia.js'),
2164
+ fileRef.text()
2165
+ ]);
2166
+ preview.controller.signal.throwIfAborted();
2167
+ completeJSON=text;
2168
+ let imageRecord;
2169
+ try {
2170
+ imageRecord = JSON.parse(completeJSON);
2171
+ } catch {
2172
+ // Mixed JSONL and ordinary text keep their complete presentation.
2173
+ imageRecord = null;
2174
+ }
2175
+ while (is.array(imageRecord) && imageRecord.length === 1) {
2176
+ imageRecord = imageRecord[0];
2177
+ }
2178
+ // A dataUrl property alone does not identify an image record.
2179
+ // Recognize its declared media type before attempting any decoding.
2180
+ if (is.string(imageRecord?.mediaType)
2181
+ && imageRecord.mediaType.toLowerCase().startsWith('image/')) {
2182
+ const image = decodeMarkdownMediaRecord(imageRecord);
2183
+ if (image?.type.startsWith('image/')) {
2184
+ return renderDescriptor(
2185
+ {kind: 'image', content: image, alt: fileRef.name},
2186
+ preview
2187
+ );
2188
+ }
2189
+ }
2158
2190
  }
2159
2191
 
2160
2192
  switch(fileRef.ext){
@@ -2168,7 +2200,7 @@
2168
2200
  case 'jsonl':
2169
2201
  case 'ndjson':{
2170
2202
  if(!transformed){
2171
- rendered.append(createPreformatted(await fileRef.text()));
2203
+ rendered.append(createPreformatted(completeJSON));
2172
2204
  break;
2173
2205
  }
2174
2206
  const records=is.array(value)?value:[];
@@ -327,3 +327,44 @@ body{
327
327
  scroll-behavior:auto !important;
328
328
  transition-duration:.01ms !important;
329
329
  }
330
+
331
+ /* Paper uses the existing base light palette regardless of the screen skin.
332
+ * Print-only importance also overrides custom-theme inline tokens; no saved
333
+ * preference, screen layout, media paint, or scrolling state changes here.
334
+ */
335
+ @media print {
336
+ @page {
337
+ margin: 15mm;
338
+ }
339
+
340
+ :root,
341
+ body,
342
+ [data-arcane-palette] {
343
+ color-scheme: light !important;
344
+ --background: rgb(255, 255, 255) !important;
345
+ --modal-background: rgb(255, 255, 255) !important;
346
+ --text-color: rgb(23, 34, 56) !important;
347
+ --primary-color: rgb(23, 34, 56) !important;
348
+ --secondary-color: rgb(229, 233, 243) !important;
349
+ --accent-color: var(--focus-color) !important;
350
+ --button-text-color: rgb(255, 255, 255) !important;
351
+ --border-color: rgb(203, 211, 226) !important;
352
+ --focus-color: rgb(118, 87, 213) !important;
353
+ --muted-text-color: rgb(95, 107, 130) !important;
354
+ --success-color: rgb(39, 122, 75) !important;
355
+ --warning-color: rgb(154, 101, 0) !important;
356
+ --danger-color: rgb(165, 56, 56) !important;
357
+ --review-color: rgb(133, 85, 165) !important;
358
+ --shadow-color: rgba(23, 34, 56, .16) !important;
359
+ --platinum: rgb(102, 112, 133) !important;
360
+ --icon-filter: none !important;
361
+ --file-icon-filter: hue-rotate(8deg) saturate(.62) brightness(.86) !important;
362
+ }
363
+
364
+ html,
365
+ body {
366
+ margin: 0;
367
+ background-color: var(--modal-background);
368
+ color: var(--text-color);
369
+ }
370
+ }
@@ -6,6 +6,20 @@ const markdownOptions = {
6
6
  async: false,
7
7
  pedantic: false,
8
8
  gfm: true,
9
+ tokenizer: {
10
+ html(src) {
11
+ const native = marked.Tokenizer.prototype.html.call(this, src);
12
+ if (native) return native;
13
+ // Quoted image values can contain blank lines that would otherwise
14
+ // split the tag into Markdown paragraphs before inline HTML parsing.
15
+ const image = /^ {0,3}<img(?:\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>(?=[ \t]*(?:\n|$))/i;
16
+ const match = image.exec(src);
17
+ if (!match) return;
18
+ const quotedValues = match[0].match(/"[^"]*"|'[^']*'/g) || [];
19
+ if (!quotedValues.some(function containsNewline(value) {return value.includes('\n');})) return;
20
+ return {type:'html', block:true, raw:match[0], text:match[0]};
21
+ }
22
+ },
9
23
  renderer: {
10
24
  link(href, title, text) {
11
25
  const link = marked.Renderer.prototype.link.call(this, href, title, text);
@@ -59,24 +59,28 @@ export async function saveMarkdownMedia({
59
59
  };
60
60
  }
61
61
 
62
- /** Read the complete local image for display or application-owned export. */
63
- export async function readMarkdownMedia(reference) {
64
- const address = parseMarkdownMediaReference(reference);
65
- if (!address) {
66
- throw new TypeError('Expected an arcane-media: reference.');
67
- }
68
- const {default: DBOPFS} = await import('./DBOPFS.js');
69
- const stored = await new DBOPFS().get(address.tableName, address.fileName);
70
- if (stored === null || stored === undefined) {
71
- throw new DOMException(`Markdown image was not found: ${reference}`, 'NotFoundError');
62
+ /**
63
+ * Decode a complete local media record or its JSON text without storage or
64
+ * network access. Unrecognized content returns null; recognized records with
65
+ * unreadable encoding throw. Records carry dataUrl and an optional mediaType;
66
+ * omitting mediaType preserves Blob's empty type. No source content is changed.
67
+ */
68
+ export function decodeMarkdownMediaRecord(value) {
69
+ let record = value;
70
+ if (is.string(record)) {
71
+ try {
72
+ record = JSON.parse(record);
73
+ } catch {
74
+ return null;
75
+ }
72
76
  }
73
77
  // DBOPFS parses JSONL/NDJSON as record arrays; restoring a parsed backup can
74
78
  // wrap its one record again. Never choose one image from multiple records.
75
- let record = is.string(stored) ? JSON.parse(stored) : stored;
76
79
  while (is.array(record) && record.length === 1) record = record[0];
77
- const encoded = /^data:[^,]*;base64,([\s\S]*)$/u.exec(record?.dataUrl);
80
+ if (!record || !is.object(record) || is.array(record) || !('dataUrl' in record)) return null;
81
+ const encoded = /^data:[^,]*;base64,([\s\S]*)$/u.exec(record.dataUrl);
78
82
  if (!encoded) {
79
- throw new TypeError(`Markdown image has an unreadable data URL: ${reference}`);
83
+ throw new TypeError('Markdown image has an unreadable data URL.');
80
84
  }
81
85
  // Decode locally; a stored string is never passed to Fetch or a network API.
82
86
  const decoded = atob(encoded[1]);
@@ -86,6 +90,30 @@ export async function readMarkdownMedia(reference) {
86
90
  return new Blob([content], {type: record.mediaType});
87
91
  }
88
92
 
93
+ /** Read the complete local image for display or application-owned export. */
94
+ export async function readMarkdownMedia(reference) {
95
+ const address = parseMarkdownMediaReference(reference);
96
+ if (!address) {
97
+ throw new TypeError('Expected an arcane-media: reference.');
98
+ }
99
+ const {default: DBOPFS} = await import('./DBOPFS.js');
100
+ const stored = await new DBOPFS().get(address.tableName, address.fileName);
101
+ if (stored === null || stored === undefined) {
102
+ throw new DOMException(`Markdown image was not found: ${reference}`, 'NotFoundError');
103
+ }
104
+ // Preserve the strict stored-JSON parser boundary for explicit reads.
105
+ const record = is.string(stored) ? JSON.parse(stored) : stored;
106
+ const unreadable = `Markdown image has an unreadable data URL: ${reference}`;
107
+ try {
108
+ const blob = decodeMarkdownMediaRecord(record);
109
+ if (blob) return blob;
110
+ } catch (error) {
111
+ if (!(error instanceof TypeError)) throw error;
112
+ throw new TypeError(unreadable, {cause: error});
113
+ }
114
+ throw new TypeError(unreadable);
115
+ }
116
+
89
117
  /**
90
118
  * Resolve only local Markdown IMG references beneath a rendered root (including
91
119
  * an IMG root). Markdown source and ordinary URLs are never changed. Independent
@@ -1,4 +1,5 @@
1
1
  import Is from 'strong-type';
2
+ import {arcaneLightThemeTokens} from '../entities/Theme.js';
2
3
 
3
4
  const is=new Is(false);
4
5
  const registries=new WeakMap();
@@ -400,17 +401,36 @@ function capture(registry,session){
400
401
  const heading=document.createElement('h1');
401
402
  heading.textContent=session.title;
402
403
  const rendered=copyRendered(session.content,state,true);
403
- styles.textContent=state.rules.join('\n');
404
+ // Screen layout and fonts describe the rendered content; screen-theme paint
405
+ // does not describe a readable paper surface. Keep this palette in the
406
+ // snapshot, including its pseudo-elements, without recoloring SVG artwork.
407
+ styles.textContent=`
408
+ :host{color-scheme:light!important;color:${arcaneLightThemeTokens.text}!important;background:${arcaneLightThemeTokens.surface}!important;font:12pt/1.5 serif}
409
+ :host>h1{margin:0 0 .75em;font-size:1.5em;line-height:1.2}
410
+ [data-arcane-print-node]:not(svg,svg *,img,video,audio),
411
+ [data-arcane-print-node]:not(svg,svg *,img,video,audio)::before,
412
+ [data-arcane-print-node]:not(svg,svg *,img,video,audio)::after{
413
+ color:${arcaneLightThemeTokens.text}!important;background-color:transparent!important;
414
+ border-color:${arcaneLightThemeTokens.border}!important;color-scheme:light!important;
415
+ -webkit-text-fill-color:currentColor!important;-webkit-text-stroke:0!important;
416
+ text-decoration-color:currentColor!important;text-shadow:none!important;box-shadow:none!important
417
+ }
418
+ a[data-arcane-print-node]:not(svg *){text-decoration:underline!important}
419
+ table[data-arcane-print-node]:not(svg *){border-collapse:collapse!important}
420
+ th[data-arcane-print-node]:not(svg *),td[data-arcane-print-node]:not(svg *){border:1px solid ${arcaneLightThemeTokens.border}!important;padding:.35em .5em!important}
421
+ ${state.rules.join('\n')}
422
+ `;
404
423
  root.append(styles,heading,rendered);
405
424
  const sheet=document.createElement('style');
406
425
  session.sheet=sheet;
407
426
  sheet.textContent=`
408
427
  [data-arcane-print-stage]{display:none!important}
409
428
  @media print{
410
- html:root,html:root>body{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important;contain:none!important}
429
+ @page{margin:15mm}
430
+ html:root,html:root>body{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important;contain:none!important;margin:0!important;padding:0!important;border:0!important;background:${arcaneLightThemeTokens.surface}!important;color:${arcaneLightThemeTokens.text}!important;color-scheme:light!important}
411
431
  html:root>body>*:not([data-arcane-print-stage]){display:none!important}
412
432
  html:root>body::before,html:root>body::after{display:none!important}
413
- html:root>body>[data-arcane-print-stage]{display:block!important;position:static!important;width:auto!important;height:auto!important;overflow:visible!important}
433
+ html:root>body>[data-arcane-print-stage]{display:block!important;position:static!important;width:auto!important;height:auto!important;overflow:visible!important;margin:0!important;padding:0!important;border:0!important}
414
434
  }
415
435
  `;
416
436
  document.head.append(sheet);