arcane-os 0.42.2 → 0.44.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.
@@ -0,0 +1,625 @@
1
+ import Is from 'strong-type';
2
+
3
+ const is=new Is(false);
4
+ const registries=new WeakMap();
5
+ let fontSequence=0;
6
+
7
+ /**
8
+ * Print current rendered DOM. Callbacks are synchronous; retain() may return a
9
+ * resource release function. print() reports a browser request, never an output.
10
+ * Native beforeprint cannot await readiness or cancel the browser's dialog.
11
+ */
12
+ export function createPrintView({
13
+ host,
14
+ content,
15
+ title,
16
+ active,
17
+ priority=0,
18
+ signal,
19
+ onError,
20
+ retain
21
+ }){
22
+ if(!host?.ownerDocument||!is.function(content)||!is.function(title)
23
+ ||!is.function(active)||!is.finite(priority)){
24
+ throw new TypeError('A print view requires a host, content/title/active callbacks, and a finite priority.');
25
+ }
26
+ if((onError!==undefined&&!is.function(onError))
27
+ ||(retain!==undefined&&!is.function(retain))){
28
+ throw new TypeError('Print error and resource-retention callbacks must be functions.');
29
+ }
30
+ const registry=documentRegistry(host.ownerDocument);
31
+ const view={host,content,title,active,priority,onError,retain,destroyed:false,pending:null};
32
+ registry.views.add(view);
33
+ signal?.addEventListener('abort',destroy,{once:true});
34
+ if(signal?.aborted){
35
+ destroy();
36
+ }
37
+ return {print,destroy};
38
+
39
+ async function print(){
40
+ if(!available(view)){
41
+ return false;
42
+ }
43
+ if(view.pending){
44
+ return view.pending;
45
+ }
46
+ view.pending=printRendered(registry,view).finally(function finishRequest(){
47
+ view.pending=null;
48
+ });
49
+ return view.pending;
50
+ }
51
+
52
+ function destroy(){
53
+ if(view.destroyed){
54
+ return;
55
+ }
56
+ view.destroyed=true;
57
+ signal?.removeEventListener('abort',destroy);
58
+ registry.views.delete(view);
59
+ if(registry.session?.view===view&&!registry.session.requested){
60
+ registry.session.controller.abort();
61
+ }
62
+ disposeRegistry(registry);
63
+ }
64
+ }
65
+
66
+ function available(view){
67
+ if(view.destroyed||!view.host.isConnected||!view.active()
68
+ ||!view.host.getClientRects().length){
69
+ return false;
70
+ }
71
+ const appearance=view.host.ownerDocument.defaultView.getComputedStyle(view.host);
72
+ return appearance.visibility!=='hidden'&&appearance.visibility!=='collapse';
73
+ }
74
+
75
+ function documentRegistry(document){
76
+ const existing=registries.get(document);
77
+ if(existing){
78
+ return existing;
79
+ }
80
+ const registry={document,views:new Set(),session:null,beforePrint,afterPrint};
81
+ registries.set(document,registry);
82
+ document.defaultView.addEventListener('beforeprint',beforePrint);
83
+ document.defaultView.addEventListener('afterprint',afterPrint);
84
+ return registry;
85
+
86
+ function beforePrint(){
87
+ let view=registry.session?.view;
88
+ try{
89
+ if(!view){
90
+ for(const candidate of registry.views){
91
+ if(available(candidate)&&(!view||candidate.priority>=view.priority)){
92
+ view=candidate;
93
+ }
94
+ }
95
+ }
96
+ if(!view){
97
+ return;
98
+ }
99
+ const session=registry.session||startSession(registry,view);
100
+ if(!session.stage){
101
+ capture(registry,session);
102
+ }
103
+ session.requested=true;
104
+ }catch(error){
105
+ if(registry.session){
106
+ registry.session.error=error;
107
+ finishSession(registry,registry.session);
108
+ }
109
+ // beforeprint is synchronous and cannot cancel the browser's dialog.
110
+ if(view?.onError){
111
+ view.onError(error);
112
+ }else{
113
+ console.error('Unable to prepare rendered content for native printing:',error);
114
+ }
115
+ }
116
+ }
117
+
118
+ function afterPrint(){
119
+ if(registry.session){
120
+ finishSession(registry,registry.session);
121
+ }
122
+ }
123
+ }
124
+
125
+ function disposeRegistry(registry){
126
+ if(registry.views.size||registry.session){
127
+ return;
128
+ }
129
+ const window=registry.document.defaultView;
130
+ window.removeEventListener('beforeprint',registry.beforePrint);
131
+ window.removeEventListener('afterprint',registry.afterPrint);
132
+ registries.delete(registry.document);
133
+ }
134
+
135
+ function startSession(registry,view){
136
+ const session={
137
+ view,
138
+ controller:new AbortController(),
139
+ requested:false,
140
+ stage:null,
141
+ sheet:null,
142
+ release:null,
143
+ fonts:[],
144
+ fontLoads:[],
145
+ error:null,
146
+ originalTitle:registry.document.title,
147
+ titleChanged:false,
148
+ finished:false
149
+ };
150
+ registry.session=session;
151
+ try{
152
+ session.release=view.retain?.()||null;
153
+ if(session.release&&!is.function(session.release)){
154
+ throw new TypeError('A print resource-retention callback must return a release function.');
155
+ }
156
+ session.content=view.content();
157
+ if(!session.content||!is.function(session.content.cloneNode)){
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
+ }
164
+ return session;
165
+ }catch(error){
166
+ finishSession(registry,session);
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ async function printRendered(registry,view){
172
+ if(registry.session){
173
+ throw new Error('A rendered print request is already being prepared or displayed.');
174
+ }
175
+ const session=startSession(registry,view);
176
+ const signal=session.controller.signal;
177
+ try{
178
+ await readyDocuments(session.content,signal);
179
+ if(session.error){
180
+ throw session.error;
181
+ }
182
+ if(session.requested){
183
+ return true;
184
+ }
185
+ if(signal.aborted||!available(view)){
186
+ finishSession(registry,session);
187
+ return false;
188
+ }
189
+ capture(registry,session);
190
+ const images=Array.from(session.stage.shadowRoot.querySelectorAll('img'));
191
+ await Promise.all(images.map(function readyImage(image){
192
+ image.loading='eager';
193
+ if(!image.currentSrc&&!image.getAttribute('src')&&!image.getAttribute('srcset')){
194
+ return undefined;
195
+ }
196
+ return abortable(image.decode(),signal);
197
+ }));
198
+ await abortable(Promise.all(session.fontLoads),signal);
199
+ await abortable(registry.document.fonts.ready,signal);
200
+ if(session.error){
201
+ throw session.error;
202
+ }
203
+ if(session.requested){
204
+ return true;
205
+ }
206
+ if(signal.aborted||!available(view)){
207
+ finishSession(registry,session);
208
+ return false;
209
+ }
210
+ try{
211
+ registry.document.defaultView.print();
212
+ }catch(error){
213
+ session.error=error;
214
+ throw error;
215
+ }
216
+ session.requested=true;
217
+ return true;
218
+ }catch(error){
219
+ if(session.requested&&!session.error){
220
+ return true;
221
+ }
222
+ const cancelled=signal.aborted&&!session.error;
223
+ finishSession(registry,session);
224
+ if(cancelled){
225
+ return false;
226
+ }
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ function finishSession(registry,session){
232
+ if(session.finished){
233
+ return;
234
+ }
235
+ session.finished=true;
236
+ session.controller.abort();
237
+ session.stage?.remove();
238
+ session.sheet?.remove();
239
+ if(session.titleChanged&&registry.document.title===session.title){
240
+ registry.document.title=session.originalTitle;
241
+ }
242
+ for(const face of session.fonts){
243
+ registry.document.fonts.delete(face);
244
+ }
245
+ if(registry.session===session){
246
+ registry.session=null;
247
+ }
248
+ try{
249
+ if(is.function(session.release)){
250
+ session.release();
251
+ }
252
+ }finally{
253
+ session.release=null;
254
+ disposeRegistry(registry);
255
+ }
256
+ }
257
+
258
+ function frameDocument(frame){
259
+ const document=frame.contentDocument;
260
+ if(!document||!['text/html','application/xhtml+xml'].includes(document.contentType)){
261
+ throw new Error('This embedded document is not accessible rendered HTML. Open it in its owning viewer to print.');
262
+ }
263
+ return document;
264
+ }
265
+
266
+ function frameReady(frame,document){
267
+ const pendingBlank=document.URL==='about:blank'
268
+ &&(frame.hasAttribute('srcdoc')||(frame.getAttribute('src')&&frame.src!=='about:blank'));
269
+ return document.readyState==='complete'&&!pendingBlank;
270
+ }
271
+
272
+ async function readyDocuments(root,signal){
273
+ const pending=[];
274
+ const document=root.ownerDocument||root;
275
+ if(document.fonts){
276
+ pending.push(abortable(document.fonts.ready,signal));
277
+ }
278
+ visit(root);
279
+ await Promise.all(pending);
280
+
281
+ function visit(node){
282
+ if(node.localName==='iframe'){
283
+ pending.push(readyFrame(node));
284
+ return;
285
+ }
286
+ for(const child of renderedChildren(node)){
287
+ visit(child);
288
+ }
289
+ }
290
+
291
+ async function readyFrame(frame){
292
+ if(!frameReady(frame,frameDocument(frame))){
293
+ await loaded(frame,signal);
294
+ }
295
+ await readyDocuments(frameDocument(frame),signal);
296
+ }
297
+ }
298
+
299
+ function loaded(element,signal){
300
+ return new Promise(function waitForLoad(resolve,reject){
301
+ function finish(error){
302
+ element.removeEventListener('load',onLoad);
303
+ element.removeEventListener('error',onError);
304
+ signal.removeEventListener('abort',onAbort);
305
+ if(error){
306
+ reject(error);
307
+ }else{
308
+ resolve();
309
+ }
310
+ }
311
+ function onLoad(){finish();}
312
+ function onError(){finish(new Error('An embedded print document could not finish loading.'));}
313
+ function onAbort(){finish(signal.reason||new DOMException('Printing was cancelled.','AbortError'));}
314
+ element.addEventListener('load',onLoad,{once:true});
315
+ element.addEventListener('error',onError,{once:true});
316
+ signal.addEventListener('abort',onAbort,{once:true});
317
+ if(signal.aborted){
318
+ onAbort();
319
+ }
320
+ });
321
+ }
322
+
323
+ function abortable(promise,signal){
324
+ return new Promise(function waitForReadiness(resolve,reject){
325
+ function onAbort(){
326
+ signal.removeEventListener('abort',onAbort);
327
+ reject(signal.reason||new DOMException('Printing was cancelled.','AbortError'));
328
+ }
329
+ signal.addEventListener('abort',onAbort,{once:true});
330
+ if(signal.aborted){
331
+ onAbort();
332
+ }
333
+ Promise.resolve(promise).then(function ready(value){
334
+ signal.removeEventListener('abort',onAbort);
335
+ resolve(value);
336
+ },function failed(error){
337
+ signal.removeEventListener('abort',onAbort);
338
+ reject(error);
339
+ });
340
+ });
341
+ }
342
+
343
+ function renderedChildren(node){
344
+ if(node.localName==='slot'){
345
+ const assigned=node.assignedNodes({flatten:true});
346
+ if(assigned.length){
347
+ return assigned;
348
+ }
349
+ }
350
+ return (node.shadowRoot||node).childNodes||[];
351
+ }
352
+
353
+ function capture(registry,session){
354
+ const document=registry.document;
355
+ const stage=document.createElement('section');
356
+ session.stage=stage;
357
+ stage.setAttribute('data-arcane-print-stage','');
358
+ const root=stage.attachShadow({mode:'open'});
359
+ const styles=document.createElement('style');
360
+ const state={document,session,rules:[],sequence:0,documents:new Map()};
361
+ const heading=document.createElement('h1');
362
+ heading.textContent=session.title;
363
+ const rendered=copyRendered(session.content,state,true);
364
+ styles.textContent=state.rules.join('\n');
365
+ root.append(styles,heading,rendered);
366
+ const sheet=document.createElement('style');
367
+ session.sheet=sheet;
368
+ sheet.textContent=`
369
+ [data-arcane-print-stage]{display:none!important}
370
+ @media print{
371
+ 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}
372
+ html:root>body>*:not([data-arcane-print-stage]){display:none!important}
373
+ 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}
375
+ }
376
+ `;
377
+ document.head.append(sheet);
378
+ document.body.append(stage);
379
+ session.originalTitle=document.title;
380
+ session.titleChanged=true;
381
+ document.title=session.title;
382
+ }
383
+
384
+ function copyRendered(source,state,root=false){
385
+ const document=state.document;
386
+ if(source.nodeType===9){
387
+ return copyRendered(source.documentElement,state,true);
388
+ }
389
+ if(source.nodeType!==1){
390
+ const clone=source.cloneNode(false);
391
+ for(const child of renderedChildren(source)){
392
+ clone.append(copyRendered(child,state));
393
+ }
394
+ return clone;
395
+ }
396
+ const name=source.localName;
397
+ if(['script','style','link','head'].includes(name)){
398
+ return document.createDocumentFragment();
399
+ }
400
+ if(name==='source'&&source.parentNode?.localName==='picture'){
401
+ // The image below carries the actual rendered currentSrc. Re-selecting
402
+ // picture candidates could change it in the print document or viewport.
403
+ return document.createDocumentFragment();
404
+ }
405
+ if(name==='object'||name==='embed'){
406
+ throw new Error('This embedded viewer does not expose rendered HTML for printing. Open it in its owning viewer to print.');
407
+ }
408
+ const sourceDocument=source.ownerDocument;
409
+ if(!state.documents.has(sourceDocument)){
410
+ state.documents.set(sourceDocument,copyFonts(sourceDocument,state));
411
+ }
412
+ const neutral=name==='iframe'||name==='html'||name==='body'||name==='picture'||name.includes('-');
413
+ let clone=neutral?document.createElement('div'):source.cloneNode(false);
414
+ if(neutral){
415
+ for(const attribute of source.attributes){
416
+ clone.setAttribute(attribute.name,attribute.value);
417
+ }
418
+ }
419
+ if(name==='canvas'){
420
+ clone=document.createElement('img');
421
+ clone.src=source.toDataURL();
422
+ }
423
+ const window=sourceDocument.defaultView;
424
+ const appearance=window.getComputedStyle(source);
425
+ for(const property of appearance){
426
+ clone.style.setProperty(property,appearance.getPropertyValue(property));
427
+ }
428
+ const families=state.documents.get(sourceDocument);
429
+ if(families.size){
430
+ clone.style.setProperty('font-family',printFontFamily(appearance.getPropertyValue('font-family'),families));
431
+ }
432
+ const selector=`[data-arcane-print-node="${String(++state.sequence)}"]`;
433
+ clone.setAttribute('data-arcane-print-node',String(state.sequence));
434
+ for(const pseudo of ['::before','::after']){
435
+ const style=window.getComputedStyle(source,pseudo);
436
+ if(style.content&&style.content!=='none'&&style.content!=='normal'){
437
+ const declarations=Array.from(style,function declaration(property){
438
+ const value=style.getPropertyValue(property);
439
+ return `${property}:${property==='font-family'?printFontFamily(value,families):value}`;
440
+ });
441
+ state.rules.push(`${selector}${pseudo}{${declarations.join(';')}}`);
442
+ }
443
+ }
444
+ if(name==='img'){
445
+ if(source.currentSrc){
446
+ clone.src=source.currentSrc;
447
+ clone.removeAttribute('srcset');
448
+ clone.removeAttribute('sizes');
449
+ }else if(source.getAttribute('src')){
450
+ clone.src=source.src;
451
+ }
452
+ clone.loading='eager';
453
+ }
454
+ if(name==='a'&&source.getAttribute('href')){
455
+ clone.href=source.href;
456
+ }
457
+ if(name==='input'||name==='textarea'||name==='select'){
458
+ clone.value=source.value;
459
+ if(name==='input'){
460
+ clone.checked=source.checked;
461
+ }
462
+ }
463
+ if(name==='option'){
464
+ clone.selected=source.selected;
465
+ }
466
+ if(!['img','svg','video','audio','input','canvas'].includes(name)){
467
+ for(const [property,value] of [
468
+ ['height','auto'],['min-height','0'],['max-height','none'],
469
+ ['width','auto'],['min-width','0'],['max-width','100%'],['overflow','visible'],
470
+ ['contain','none'],['content-visibility','visible'],['grid-template-rows','none'],
471
+ ['break-inside','auto']
472
+ ]){
473
+ clone.style.setProperty(property,value);
474
+ }
475
+ }else if(name==='img'||name==='canvas'){
476
+ clone.style.setProperty('max-width','100%');
477
+ clone.style.setProperty('height','auto');
478
+ }
479
+ if(['absolute','fixed','sticky'].includes(appearance.position)){
480
+ clone.style.setProperty('position','static');
481
+ }
482
+ if(root||name==='iframe'){
483
+ clone.hidden=false;
484
+ clone.style.setProperty('display',appearance.display==='none'?'block':appearance.display);
485
+ clone.style.setProperty('visibility','visible');
486
+ }
487
+ if(name==='pre'){
488
+ clone.style.setProperty('white-space','pre-wrap');
489
+ clone.style.setProperty('overflow-wrap','anywhere');
490
+ }
491
+ if(name==='table'){
492
+ clone.style.setProperty('table-layout','auto');
493
+ }
494
+ if(name==='td'||name==='th'){
495
+ clone.style.setProperty('overflow-wrap','anywhere');
496
+ clone.style.setProperty('white-space','normal');
497
+ }
498
+ if(name==='iframe'){
499
+ const embedded=frameDocument(source);
500
+ if(!frameReady(source,embedded)){
501
+ throw new Error('The embedded HTML preview is still loading. Print again after it finishes.');
502
+ }
503
+ clone.append(copyRendered(embedded,state,true));
504
+ }else{
505
+ for(const child of renderedChildren(source)){
506
+ clone.append(copyRendered(child,state));
507
+ }
508
+ }
509
+ return clone;
510
+ }
511
+
512
+ function copyFonts(source,state){
513
+ const families=new Map();
514
+ if(source===state.document){
515
+ return families;
516
+ }
517
+ let definitions=null;
518
+ const visited=new Set();
519
+ for(const face of source.fonts){
520
+ if(face.status!=='loaded'||state.document.fonts.has(face)){
521
+ continue;
522
+ }
523
+ if(!definitions){
524
+ definitions=[];
525
+ for(const sheet of source.styleSheets){
526
+ readSheet(sheet);
527
+ }
528
+ }
529
+ const definition=definitions.find(function matchingFont({style}){
530
+ return fontFamilyName(style.getPropertyValue('font-family'))===fontFamilyName(face.family)
531
+ &&(style.getPropertyValue('font-style')||'normal')===face.style
532
+ &&(style.getPropertyValue('font-weight')||'normal')===face.weight
533
+ &&(style.getPropertyValue('font-stretch')||'normal')===face.stretch
534
+ &&(style.getPropertyValue('unicode-range')||'U+0-10FFFF')===face.unicodeRange;
535
+ });
536
+ if(!definition){
537
+ throw new Error('An embedded document font does not expose its source. Open that document in its owning viewer to print.');
538
+ }
539
+ const family=fontFamilyName(face.family);
540
+ if(!families.has(family)){
541
+ families.set(family,`ArcanePrint-${String(++fontSequence)}`);
542
+ }
543
+ const src=definition.style.getPropertyValue('src').replace(
544
+ /url\("((?:\\.|[^"\\])*)"\)/gu,
545
+ function absoluteFontURL(match,value){
546
+ return `url(${JSON.stringify(new URL(cssString(value),definition.base).href)})`;
547
+ }
548
+ );
549
+ const copy=new state.document.defaultView.FontFace(families.get(family),src,{
550
+ style:face.style,
551
+ weight:face.weight,
552
+ stretch:face.stretch,
553
+ unicodeRange:face.unicodeRange,
554
+ featureSettings:face.featureSettings,
555
+ variationSettings:face.variationSettings,
556
+ display:face.display
557
+ });
558
+ state.document.fonts.add(copy);
559
+ state.session.fonts.push(copy);
560
+ // The source has already loaded this face. The snapshot may reuse the
561
+ // browser's resource cache; explicit printing observes actual readiness.
562
+ const ready=copy.load();
563
+ state.session.fontLoads.push(ready);
564
+ void ready.catch(function observeNativeFontError(error){
565
+ if(state.session.requested&&!state.session.finished){
566
+ if(state.session.view.onError){
567
+ state.session.view.onError(error);
568
+ }else{
569
+ console.error('Unable to load an embedded print font:',error);
570
+ }
571
+ }
572
+ });
573
+ }
574
+ return families;
575
+
576
+ function readSheet(sheet){
577
+ if(visited.has(sheet)){
578
+ return;
579
+ }
580
+ visited.add(sheet);
581
+ let rules;
582
+ try{
583
+ rules=sheet.cssRules;
584
+ }catch(error){
585
+ if(error.name==='SecurityError'){
586
+ return;
587
+ }
588
+ throw error;
589
+ }
590
+ readRules(rules,sheet.href||source.baseURI);
591
+ }
592
+
593
+ function readRules(rules,base){
594
+ for(const rule of rules){
595
+ if(rule.type===5){
596
+ definitions.push({style:rule.style,base});
597
+ }else if(rule.styleSheet){
598
+ readSheet(rule.styleSheet);
599
+ }else if(rule.cssRules){
600
+ readRules(rule.cssRules,base);
601
+ }
602
+ }
603
+ }
604
+ }
605
+
606
+ function cssString(value){
607
+ return value.replace(/\\([\da-f]{1,6}\s?|.)/giu,function cssEscape(escape,character){
608
+ return /^[\da-f]/iu.test(character)
609
+ ?String.fromCodePoint(parseInt(character.trim(),16)||0xfffd)
610
+ :character;
611
+ });
612
+ }
613
+
614
+ function fontFamilyName(value){
615
+ const name=value.trim();
616
+ return cssString(name.replace(/^(?:"(.*)"|'(.*)')$/u,function unquote(match,double,single){
617
+ return double??single;
618
+ })).toLowerCase();
619
+ }
620
+
621
+ function printFontFamily(value,families){
622
+ return value.replace(/(?:[^,"']|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')+/gu,function family(name){
623
+ return families.get(fontFamilyName(name))||name;
624
+ });
625
+ }