arcane-os 0.44.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 +42 -0
- package/README.md +1 -1
- package/docs/reference/runtime-components.md +78 -7
- package/docs/reference/runtime-modules.md +139 -7
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +27 -6
- package/runtime/arcane/components/file-manager.html +146 -8
- package/runtime/arcane/components/markdown-document.html +31 -4
- package/runtime/arcane/components/markdown-editor.html +113 -4
- package/runtime/arcane/css/theme.css +41 -0
- package/runtime/arcane/entities/Chat.js +6 -4
- package/runtime/arcane/modules/MD.js +14 -0
- package/runtime/arcane/modules/MarkdownMedia.js +239 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +22 -8
- package/runtime/arcane/modules/PrintView.js +74 -15
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
const referencePrefix = 'arcane-media:';
|
|
5
|
+
|
|
6
|
+
/** Return the local record address, or null for an ordinary image URL. */
|
|
7
|
+
export function parseMarkdownMediaReference(reference) {
|
|
8
|
+
if (!is.string(reference) || !reference.startsWith(referencePrefix)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const address = reference.substring(referencePrefix.length);
|
|
12
|
+
const separator = address.indexOf('/');
|
|
13
|
+
if (separator < 0) {
|
|
14
|
+
throw new TypeError('A Markdown media reference needs a table and filename.');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
tableName: decodeURIComponent(address.substring(0, separator)),
|
|
18
|
+
fileName: decodeURIComponent(address.substring(separator + 1))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save a complete image in the app's existing DBOPFS database. The returned
|
|
24
|
+
* reference belongs in Markdown; the JSON-compatible record belongs in its
|
|
25
|
+
* separate table so the existing database backup can include it unchanged.
|
|
26
|
+
* Applications own filename reuse, entry association, and export selection.
|
|
27
|
+
* The promise settles the durable write; this operation has no abort option.
|
|
28
|
+
*/
|
|
29
|
+
export async function saveMarkdownMedia({
|
|
30
|
+
blob,
|
|
31
|
+
tableName = 'markdown-media',
|
|
32
|
+
fileName = `${crypto.randomUUID()}.json`
|
|
33
|
+
}) {
|
|
34
|
+
const reference = `${referencePrefix}${encodeURIComponent(tableName)}/${encodeURIComponent(fileName)}`;
|
|
35
|
+
const encoding = new Promise(function encodeImage(resolve, reject) {
|
|
36
|
+
const reader = new FileReader();
|
|
37
|
+
reader.onload = function imageEncoded() {
|
|
38
|
+
resolve(reader.result);
|
|
39
|
+
};
|
|
40
|
+
reader.onerror = function imageEncodingFailed() {
|
|
41
|
+
reject(reader.error || new Error('Image encoding failed.'));
|
|
42
|
+
};
|
|
43
|
+
reader.onabort = function imageEncodingAborted() {
|
|
44
|
+
reject(new DOMException('Image encoding was aborted.', 'AbortError'));
|
|
45
|
+
};
|
|
46
|
+
reader.readAsDataURL(blob);
|
|
47
|
+
});
|
|
48
|
+
const [dataUrl, {default: DBOPFS}] = await Promise.all([
|
|
49
|
+
encoding,
|
|
50
|
+
import('./DBOPFS.js')
|
|
51
|
+
]);
|
|
52
|
+
const mediaType = blob.type || 'application/octet-stream';
|
|
53
|
+
await new DBOPFS().set(tableName, fileName, {mediaType, dataUrl});
|
|
54
|
+
return {
|
|
55
|
+
reference,
|
|
56
|
+
tableName,
|
|
57
|
+
fileName,
|
|
58
|
+
mediaType
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// DBOPFS parses JSONL/NDJSON as record arrays; restoring a parsed backup can
|
|
78
|
+
// wrap its one record again. Never choose one image from multiple records.
|
|
79
|
+
while (is.array(record) && record.length === 1) record = record[0];
|
|
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);
|
|
82
|
+
if (!encoded) {
|
|
83
|
+
throw new TypeError('Markdown image has an unreadable data URL.');
|
|
84
|
+
}
|
|
85
|
+
// Decode locally; a stored string is never passed to Fetch or a network API.
|
|
86
|
+
const decoded = atob(encoded[1]);
|
|
87
|
+
const content = Uint8Array.from(decoded, function imageCharacter(character) {
|
|
88
|
+
return character.charCodeAt(0);
|
|
89
|
+
});
|
|
90
|
+
return new Blob([content], {type: record.mediaType});
|
|
91
|
+
}
|
|
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
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve only local Markdown IMG references beneath a rendered root (including
|
|
119
|
+
* an IMG root). Markdown source and ordinary URLs are never changed. Independent
|
|
120
|
+
* reads start together; ready waits for decoding or display cancellation.
|
|
121
|
+
* Failures reject with AggregateError.failures [{image, reference, reason}];
|
|
122
|
+
* successful siblings remain visible until their owner is destroyed.
|
|
123
|
+
*
|
|
124
|
+
* destroy() prevents late assignments and releases owned object URLs. A print
|
|
125
|
+
* owner calls retain() before copying the rendered DOM and releases its returned
|
|
126
|
+
* idempotent callback after printing, allowing URLs to outlive the source view.
|
|
127
|
+
* Abort promptly rejects display readiness. An already-started DBOPFS read
|
|
128
|
+
* remains observed and settles independently; its late result is not displayed.
|
|
129
|
+
*/
|
|
130
|
+
export function hydrateMarkdownMedia(root, {signal} = {}) {
|
|
131
|
+
const images = [...root.querySelectorAll('img')];
|
|
132
|
+
if (root.localName === 'img') images.unshift(root);
|
|
133
|
+
const owned = [];
|
|
134
|
+
const displayCancellations = new Set();
|
|
135
|
+
let destroyed = false;
|
|
136
|
+
let retainers = 0;
|
|
137
|
+
let destructionReason;
|
|
138
|
+
|
|
139
|
+
function releaseURLs() {
|
|
140
|
+
if (!destroyed || retainers > 0) return;
|
|
141
|
+
for (const {image, url} of owned) {
|
|
142
|
+
if (image.getAttribute('src') === url) image.removeAttribute('src');
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
}
|
|
145
|
+
owned.length = 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function destroy() {
|
|
149
|
+
if (destroyed) return;
|
|
150
|
+
destroyed = true;
|
|
151
|
+
destructionReason = signal?.aborted
|
|
152
|
+
? signal.reason
|
|
153
|
+
: new DOMException('Markdown media display was destroyed.', 'AbortError');
|
|
154
|
+
signal?.removeEventListener('abort', destroy);
|
|
155
|
+
for (const cancel of displayCancellations) cancel();
|
|
156
|
+
displayCancellations.clear();
|
|
157
|
+
releaseURLs();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function retain() {
|
|
161
|
+
if (destroyed) throw destructionReason;
|
|
162
|
+
retainers += 1;
|
|
163
|
+
let released = false;
|
|
164
|
+
return function releaseMarkdownMedia() {
|
|
165
|
+
if (released) return;
|
|
166
|
+
released = true;
|
|
167
|
+
retainers -= 1;
|
|
168
|
+
releaseURLs();
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function waitForDisplay(operation) {
|
|
173
|
+
return new Promise(function observeDisplayOperation(resolve, reject) {
|
|
174
|
+
let settled = false;
|
|
175
|
+
function settle(callback, value) {
|
|
176
|
+
if (settled) return;
|
|
177
|
+
settled = true;
|
|
178
|
+
displayCancellations.delete(cancelDisplay);
|
|
179
|
+
callback(value);
|
|
180
|
+
}
|
|
181
|
+
function cancelDisplay() {
|
|
182
|
+
settle(reject, destructionReason);
|
|
183
|
+
}
|
|
184
|
+
displayCancellations.add(cancelDisplay);
|
|
185
|
+
// Both outcomes remain observed after display cancellation; this
|
|
186
|
+
// wait does not claim to cancel the underlying storage or decode.
|
|
187
|
+
Promise.resolve(operation).then(
|
|
188
|
+
function displayOperationFinished(value) {settle(resolve, value);},
|
|
189
|
+
function displayOperationFailed(error) {settle(reject, error);}
|
|
190
|
+
);
|
|
191
|
+
if (destroyed) cancelDisplay();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function loadImage(image, reference) {
|
|
196
|
+
if (destroyed) throw destructionReason;
|
|
197
|
+
const blob = await waitForDisplay(readMarkdownMedia(reference));
|
|
198
|
+
if (destroyed) throw destructionReason;
|
|
199
|
+
const url = URL.createObjectURL(blob);
|
|
200
|
+
owned.push({image, url});
|
|
201
|
+
image.setAttribute('src', url);
|
|
202
|
+
await waitForDisplay(image.decode());
|
|
203
|
+
if (destroyed) throw destructionReason;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
signal?.addEventListener('abort', destroy, {once: true});
|
|
207
|
+
if (signal?.aborted) destroy();
|
|
208
|
+
const pending = [];
|
|
209
|
+
for (const image of images) {
|
|
210
|
+
const reference = image.getAttribute('src');
|
|
211
|
+
if (!is.string(reference) || !reference.startsWith(referencePrefix)) continue;
|
|
212
|
+
// Remove the custom scheme before the first asynchronous storage wait.
|
|
213
|
+
if (!destroyed) image.removeAttribute('src');
|
|
214
|
+
pending.push({image, reference, operation: loadImage(image, reference)});
|
|
215
|
+
}
|
|
216
|
+
const ready = Promise.allSettled(pending.map(function imageOperation(item) {
|
|
217
|
+
return item.operation;
|
|
218
|
+
})).then(function imagesSettled(results) {
|
|
219
|
+
const failures = [];
|
|
220
|
+
for (const [index, result] of results.entries()) {
|
|
221
|
+
if (result.status === 'rejected') {
|
|
222
|
+
failures.push({
|
|
223
|
+
image: pending[index].image,
|
|
224
|
+
reference: pending[index].reference,
|
|
225
|
+
reason: result.reason
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (failures.length > 0) {
|
|
230
|
+
const error = new AggregateError(failures.map(function imageFailure(failure) {
|
|
231
|
+
return failure.reason;
|
|
232
|
+
}), 'Some local Markdown images could not be displayed.');
|
|
233
|
+
error.failures = failures;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
if (destroyed) throw destructionReason;
|
|
237
|
+
});
|
|
238
|
+
return {ready, destroy, retain};
|
|
239
|
+
}
|
|
@@ -556,6 +556,7 @@ class PersistentAIChatSession{
|
|
|
556
556
|
/** Persists one model-authored opening while retaining none of its bootstrap request. */
|
|
557
557
|
async open(input){
|
|
558
558
|
const settings=normalizeOpening(input);
|
|
559
|
+
const retainOpening=this.#entity.persist===true;
|
|
559
560
|
const name=this.aiName;
|
|
560
561
|
if(this.#pending){
|
|
561
562
|
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
@@ -565,7 +566,7 @@ class PersistentAIChatSession{
|
|
|
565
566
|
try{
|
|
566
567
|
await this.ready();
|
|
567
568
|
if(this.#historyError)throw this.#historyError;
|
|
568
|
-
if(this.#entity.persist!==true){
|
|
569
|
+
if(!retainOpening||this.#entity.persist!==true){
|
|
569
570
|
throw coded(
|
|
570
571
|
new Error('The model-authored chat opening requires durable chat persistence.'),
|
|
571
572
|
'AI_CHAT_PERSISTENCE_UNAVAILABLE',
|
|
@@ -576,6 +577,15 @@ class PersistentAIChatSession{
|
|
|
576
577
|
settings.message,
|
|
577
578
|
{request:settings.request,signal:settings.signal},
|
|
578
579
|
);
|
|
580
|
+
if(this.#entity.persist!==true){
|
|
581
|
+
throw coded(
|
|
582
|
+
new Error(
|
|
583
|
+
'The model-authored chat opening requires durable chat persistence.',
|
|
584
|
+
{cause:prepared.response},
|
|
585
|
+
),
|
|
586
|
+
'AI_CHAT_PERSISTENCE_UNAVAILABLE',
|
|
587
|
+
);
|
|
588
|
+
}
|
|
579
589
|
await this.#entity.addAIMessage(
|
|
580
590
|
prepared.response.message.content,
|
|
581
591
|
{extractMemory:false,persist:true,name},
|
|
@@ -597,6 +607,7 @@ class PersistentAIChatSession{
|
|
|
597
607
|
|
|
598
608
|
async #requestTurn(input,streamHandlers=null){
|
|
599
609
|
const settings=normalizeSend(input);
|
|
610
|
+
const retainTurn=settings.messagePersist&&Boolean(this.#entity.persist);
|
|
600
611
|
const name=this.aiName;
|
|
601
612
|
if(this.#pending){
|
|
602
613
|
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
@@ -621,7 +632,7 @@ class PersistentAIChatSession{
|
|
|
621
632
|
if(persistence===undefined||submittedIds.has(message.tool_call_id)){
|
|
622
633
|
throw coded(new TypeError('A tool message has no unique pending structural tool call.'),'AI_CHAT_INVALID_TOOL_MESSAGE');
|
|
623
634
|
}
|
|
624
|
-
if(persistence!==settings.messagePersist){
|
|
635
|
+
if(retainTurn&&persistence!==settings.messagePersist){
|
|
625
636
|
throw coded(
|
|
626
637
|
new TypeError('A tool result must use the persistence of its assistant tool call.'),
|
|
627
638
|
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
@@ -674,10 +685,10 @@ class PersistentAIChatSession{
|
|
|
674
685
|
);
|
|
675
686
|
}
|
|
676
687
|
}
|
|
677
|
-
if(!
|
|
688
|
+
if(!retainTurn||!this.#entity.persist){
|
|
678
689
|
prepared.rollback();
|
|
679
690
|
prepared=null;
|
|
680
|
-
return assistantDisplayResponse(result,name);
|
|
691
|
+
return {...assistantDisplayResponse(result,name),retained:false};
|
|
681
692
|
}
|
|
682
693
|
await this.#entity.addTurn({
|
|
683
694
|
assistantMessage:result.message,
|
|
@@ -706,10 +717,13 @@ class PersistentAIChatSession{
|
|
|
706
717
|
}
|
|
707
718
|
const transcript=this.#entity.transcript;
|
|
708
719
|
const assistantRecord=transcript.at(-1);
|
|
709
|
-
return
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
return {
|
|
721
|
+
...assistantDisplayResponse(
|
|
722
|
+
committed,name,
|
|
723
|
+
assistantRecord?.role==='assistant'?assistantRecord.timestamp:undefined,
|
|
724
|
+
),
|
|
725
|
+
retained:true,
|
|
726
|
+
};
|
|
713
727
|
}catch(error){
|
|
714
728
|
prepared?.rollback();
|
|
715
729
|
throw error;
|
|
@@ -1,12 +1,14 @@
|
|
|
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();
|
|
5
6
|
let fontSequence=0;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Print current rendered DOM.
|
|
9
|
-
*
|
|
9
|
+
* Print current rendered DOM. prepare(signal) may await explicit-print readiness;
|
|
10
|
+
* other callbacks are synchronous. retain() may return a resource release function.
|
|
11
|
+
* print() reports a browser request, never an output.
|
|
10
12
|
* Native beforeprint cannot await readiness or cancel the browser's dialog.
|
|
11
13
|
*/
|
|
12
14
|
export function createPrintView({
|
|
@@ -17,7 +19,8 @@ export function createPrintView({
|
|
|
17
19
|
priority=0,
|
|
18
20
|
signal,
|
|
19
21
|
onError,
|
|
20
|
-
retain
|
|
22
|
+
retain,
|
|
23
|
+
prepare
|
|
21
24
|
}){
|
|
22
25
|
if(!host?.ownerDocument||!is.function(content)||!is.function(title)
|
|
23
26
|
||!is.function(active)||!is.finite(priority)){
|
|
@@ -27,8 +30,11 @@ export function createPrintView({
|
|
|
27
30
|
||(retain!==undefined&&!is.function(retain))){
|
|
28
31
|
throw new TypeError('Print error and resource-retention callbacks must be functions.');
|
|
29
32
|
}
|
|
33
|
+
if(prepare!==undefined&&!is.function(prepare)){
|
|
34
|
+
throw new TypeError('A print preparation callback must be a function.');
|
|
35
|
+
}
|
|
30
36
|
const registry=documentRegistry(host.ownerDocument);
|
|
31
|
-
const view={host,content,title,active,priority,onError,retain,destroyed:false,pending:null};
|
|
37
|
+
const view={host,content,title,active,priority,onError,retain,prepare,destroyed:false,pending:null};
|
|
32
38
|
registry.views.add(view);
|
|
33
39
|
signal?.addEventListener('abort',destroy,{once:true});
|
|
34
40
|
if(signal?.aborted){
|
|
@@ -98,6 +104,10 @@ function documentRegistry(document){
|
|
|
98
104
|
}
|
|
99
105
|
const session=registry.session||startSession(registry,view);
|
|
100
106
|
if(!session.stage){
|
|
107
|
+
if(view.prepare){
|
|
108
|
+
session.content=readContent(view);
|
|
109
|
+
session.title=readTitle(view);
|
|
110
|
+
}
|
|
101
111
|
capture(registry,session);
|
|
102
112
|
}
|
|
103
113
|
session.requested=true;
|
|
@@ -153,14 +163,8 @@ function startSession(registry,view){
|
|
|
153
163
|
if(session.release&&!is.function(session.release)){
|
|
154
164
|
throw new TypeError('A print resource-retention callback must return a release function.');
|
|
155
165
|
}
|
|
156
|
-
session.content=view
|
|
157
|
-
|
|
158
|
-
throw new TypeError('Print content must be a rendered DOM Node.');
|
|
159
|
-
}
|
|
160
|
-
session.title=view.title();
|
|
161
|
-
if(!is.string(session.title)){
|
|
162
|
-
throw new TypeError('A print title must be a string.');
|
|
163
|
-
}
|
|
166
|
+
session.content=readContent(view);
|
|
167
|
+
session.title=readTitle(view);
|
|
164
168
|
return session;
|
|
165
169
|
}catch(error){
|
|
166
170
|
finishSession(registry,session);
|
|
@@ -168,6 +172,22 @@ function startSession(registry,view){
|
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
function readContent(view){
|
|
176
|
+
const content=view.content();
|
|
177
|
+
if(!content||!is.function(content.cloneNode)){
|
|
178
|
+
throw new TypeError('Print content must be a rendered DOM Node.');
|
|
179
|
+
}
|
|
180
|
+
return content;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readTitle(view){
|
|
184
|
+
const title=view.title();
|
|
185
|
+
if(!is.string(title)){
|
|
186
|
+
throw new TypeError('A print title must be a string.');
|
|
187
|
+
}
|
|
188
|
+
return title;
|
|
189
|
+
}
|
|
190
|
+
|
|
171
191
|
async function printRendered(registry,view){
|
|
172
192
|
if(registry.session){
|
|
173
193
|
throw new Error('A rendered print request is already being prepared or displayed.');
|
|
@@ -186,6 +206,23 @@ async function printRendered(registry,view){
|
|
|
186
206
|
finishSession(registry,session);
|
|
187
207
|
return false;
|
|
188
208
|
}
|
|
209
|
+
if(view.prepare){
|
|
210
|
+
await abortable(view.prepare(signal),signal);
|
|
211
|
+
if(session.error){
|
|
212
|
+
throw session.error;
|
|
213
|
+
}
|
|
214
|
+
if(session.requested){
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if(signal.aborted||!available(view)){
|
|
218
|
+
finishSession(registry,session);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
// Read and capture together: a changing view can reject pending
|
|
222
|
+
// content synchronously without an unobserved readiness gap.
|
|
223
|
+
session.content=readContent(view);
|
|
224
|
+
session.title=readTitle(view);
|
|
225
|
+
}
|
|
189
226
|
capture(registry,session);
|
|
190
227
|
const images=Array.from(session.stage.shadowRoot.querySelectorAll('img'));
|
|
191
228
|
await Promise.all(images.map(function readyImage(image){
|
|
@@ -224,6 +261,9 @@ async function printRendered(registry,view){
|
|
|
224
261
|
if(cancelled){
|
|
225
262
|
return false;
|
|
226
263
|
}
|
|
264
|
+
if(view.prepare&&session.error){
|
|
265
|
+
throw session.error;
|
|
266
|
+
}
|
|
227
267
|
throw error;
|
|
228
268
|
}
|
|
229
269
|
}
|
|
@@ -361,17 +401,36 @@ function capture(registry,session){
|
|
|
361
401
|
const heading=document.createElement('h1');
|
|
362
402
|
heading.textContent=session.title;
|
|
363
403
|
const rendered=copyRendered(session.content,state,true);
|
|
364
|
-
|
|
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
|
+
`;
|
|
365
423
|
root.append(styles,heading,rendered);
|
|
366
424
|
const sheet=document.createElement('style');
|
|
367
425
|
session.sheet=sheet;
|
|
368
426
|
sheet.textContent=`
|
|
369
427
|
[data-arcane-print-stage]{display:none!important}
|
|
370
428
|
@media print{
|
|
371
|
-
|
|
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}
|
|
372
431
|
html:root>body>*:not([data-arcane-print-stage]){display:none!important}
|
|
373
432
|
html:root>body::before,html:root>body::after{display:none!important}
|
|
374
|
-
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}
|
|
375
434
|
}
|
|
376
435
|
`;
|
|
377
436
|
document.head.append(sheet);
|