forked from asgeirtj/system_prompts_leaks
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdoc-page.js
More file actions
454 lines (435 loc) · 20.5 KB
/
Copy pathdoc-page.js
File metadata and controls
454 lines (435 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).
/* BEGIN USAGE */
/**
* <doc-page> — paged-document shell for printable HTML.
*
* On screen the document renders as a single continuous sheet on a desk
* background (Google Docs' pageless view): you scroll one tall page card.
* There is no manual page-splitting — write the whole document as normal
* flow inside <doc-page> and the browser's print engine paginates it at
* export.
*
* At print the component injects `@page { size: …; margin: 0 }` (which
* leaves Chrome no margin box to draw its date/URL/page-count header in)
* and moves the visual margin onto the sheet's own padding, so the printed
* page has the same inset you see on screen. Standard break-hygiene rules
* (`break-inside: avoid` on figures, code blocks, images and table rows;
* `orphans/widows: 3`) are applied so paragraphs and groups split cleanly.
* On screen and at print, headings default to `text-wrap: balance` and
* body text (p, li, blockquote, figcaption) to `text-wrap: pretty`, so
* the document avoids widowed/orphaned words; the defaults have zero
* specificity, so any text-wrap you declare on those elements wins.
* The component also marks the document as owning its print CSS (a
* `meta[name="omelette-owns-print"]` it injects at runtime), so the
* PDF export never injects page-geometry CSS of its own on top.
*
* Usage:
* <style>doc-page:not(:defined){visibility:hidden}</style>
* <doc-page size="letter" margin="0.75in">
* <h1>Title</h1>
* <p>…body…</p>
* </doc-page>
* <script src="doc-page.js"></script>
*
* Attributes:
* size — letter | a4 | legal (default letter)
* orientation — portrait (default) | landscape. For documents built to
* export, always set it explicitly. landscape swaps the named
* size's dimensions (letter landscape prints 11in × 8.5in).
* width / height — explicit CSS lengths, override `size` and
* `orientation`: the page IS the design's size (a poster
* printed at its true dimensions). With both set, the component
* also declares the page box as the preview size (a
* `meta[name="omelette-fixed-size"]` it injects at runtime,
* never overriding one you author), so the in-app preview
* scales the whole sheet into view.
* content-width / content-height — the design's own fixed dimensions
* (CSS lengths), for scaling a fixed-size design ONTO the named
* paper: content lays out at exactly this size, and the
* component scales it to fit the printable area (centered
* horizontally, top-aligned), so e.g. a 960px-wide poster lands
* on one Letter page. Both must be set; they do not change the
* page box — `size`/`orientation` (or `width`/`height`)
* still name the paper. For pages WITHOUT running
* header/footer slots — the fit box fills the printable area
* and does not subtract slot heights.
* margin — printable inset on every page (default 0.75in); margin="0"
* makes pages full-bleed (content then owns its own insets)
*
* Running header/footer (optional): give an element `slot="header"` or
* `slot="footer"` and it repeats on every printed page via
* `position: fixed`. To keep body text from sliding under it, the
* component prints inside a single-cell table whose <thead>/<tfoot> are
* spacers sized to the header/footer height — browsers repeat thead/tfoot
* on every page, so each sheet's content starts below the header and ends
* above the footer. On screen the header/footer render once at the
* top/bottom of the sheet.
*
* Print best practices for the content you author:
* - Multi-column text: use CSS columns (`column-count` +
* `column-gap`), never side-by-side flex/grid columns — only real
* CSS columns flow and break across pages. `column-span: all` lets
* a heading span the columns; `hyphens: auto` (needs `lang` on
* the html element) keeps narrow columns readable.
* - Page breaks: `break-before: page` on an element that must start
* a new page (a chapter, an appendix). Add your own kept-together
* blocks (callouts, stat tiles, cards) to a `break-inside: avoid`
* rule, and keep each one shorter than a page.
* - Extend `orphans: 3; widows: 3` to any custom text blocks you add
* (p and li are covered by default).
* - Give long tables a <thead> — browsers repeat it on every printed
* page.
* - No `position: fixed`/`sticky` and no viewport units in content:
* fixed elements stamp every printed page (running headers/footers go
* in the component's slots) and `100vh` mis-sizes at print.
*
* Author content as static HTML so the user can click-to-edit any text
* directly. Do not set width/padding/background on the document body —
* the component owns the sheet box.
*/
/* END USAGE */
(() => {
const PAPER = {
letter: ['8.5in', '11in'],
a4: ['210mm', '297mm'],
legal: ['8.5in', '14in'],
};
const CSS_LENGTH = /^\d+(\.\d+)?(px|in|mm|cm|pt|pc)$/;
// Unitless "0" is a valid CSS length and the natural way to write
// margin="0"; normalise it to 0px so max()/calc() (which reject a bare
// number) keep working.
const safeLen = (v, fb) => {
v = (v || '').trim();
return v === '0' ? '0px' : CSS_LENGTH.test(v) ? v : fb;
};
// CSS length → px number (CSS absolute units are exact: 1in = 96px).
// Returns NaN for anything safeLen would reject — callers gate on it.
const PX_PER = { px: 1, in: 96, mm: 96 / 25.4, cm: 96 / 2.54, pt: 96 / 72, pc: 16 };
const toPx = (v) => {
const m = /^(\d+(?:\.\d+)?)(px|in|mm|cm|pt|pc)$/.exec((v || '').trim());
return m ? parseFloat(m[1]) * PX_PER[m[2]] : NaN;
};
const stylesheet = `
:host {
position: relative;
display: block;
/* When the viewport is narrower than the page, grow to wrap the
* sheet (plus this padding) instead of staying viewport-width, so
* the desk background and right margin reach the sheet's far edge
* in the horizontal scroll. */
min-width: max-content;
min-height: 100vh;
background: #ece8dd;
padding: 48px 24px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
--doc-page-w: 8.5in;
--doc-page-h: 11in;
--doc-page-margin: 0.75in;
--doc-hdr-h: 0px;
--doc-ftr-h: 0px;
--doc-hdr-pad: 0px;
--doc-ftr-pad: 0px;
}
.sheet {
width: var(--doc-page-w);
margin: 0 auto;
background: #fff;
box-shadow: 0 2px 14px rgba(20, 20, 19, 0.12);
border-radius: 2px;
box-sizing: border-box;
padding: var(--doc-page-margin);
}
.frame { width: 100%; border-collapse: collapse; }
/* Scaled-fit mode (content-width/content-height): the inner .fit box
* lays the content out at its authored fixed size and scales it onto
* the printable area; .fit-box reserves the scaled footprint in flow
* (transforms don't affect layout) and centers it. Without the mode,
* both divs are unstyled block pass-throughs. */
.fit-mode .fit-box {
width: calc(var(--doc-fit-w) * var(--doc-fit-scale));
height: calc(var(--doc-fit-h) * var(--doc-fit-scale));
margin: 0 auto;
break-inside: avoid;
}
.fit-mode .fit {
width: var(--doc-fit-w);
height: var(--doc-fit-h);
transform: scale(var(--doc-fit-scale));
transform-origin: top left;
}
.frame td, .frame th { padding: 0; text-align: left; font-weight: inherit; }
.hdr-space { height: var(--doc-hdr-h); }
.ftr-space { height: var(--doc-ftr-h); }
::slotted([slot="header"]),
::slotted([slot="footer"]) { display: block; box-sizing: border-box; }
@media print {
:host { background: none; padding: 0; min-width: 0; min-height: 0; }
.sheet {
width: auto; margin: 0; box-shadow: none; border-radius: 0;
padding: 0 var(--doc-page-margin);
}
/* The thead/tfoot spacers repeat on every page, so they carry the
* vertical page margin (which the sheet's own padding cannot, since
* that padding is consumed once on the first/last page). The running
* header/footer are fixed inside that band. */
/* The 0.35in is breathing room between a running header/footer and
* the body; without one the spacer is exactly the page margin, so a
* margin="0" full-bleed document gets truly full-bleed pages. */
.hdr-space { height: max(var(--doc-page-margin), calc(var(--doc-hdr-h) + var(--doc-hdr-pad))); }
.ftr-space { height: max(var(--doc-page-margin), calc(var(--doc-ftr-h) + var(--doc-ftr-pad))); }
::slotted([slot="header"]) {
position: fixed; top: 0; left: 0; right: 0; margin: 0;
padding: calc(var(--doc-page-margin) * 0.45) var(--doc-page-margin) 0;
}
::slotted([slot="footer"]) {
position: fixed; bottom: 0; left: 0; right: 0; margin: 0;
padding: 0 var(--doc-page-margin) calc(var(--doc-page-margin) * 0.45);
}
}
`;
class DocPage extends HTMLElement {
static get observedAttributes() {
return ['size', 'width', 'height', 'margin', 'orientation', 'content-width', 'content-height'];
}
constructor() {
super();
this._root = this.attachShadow({ mode: 'open' });
this._mo = typeof MutationObserver === 'function'
? new MutationObserver(() => this._scheduleMeasure())
: null;
}
/** The named paper's [w, h], swapped when orientation="landscape".
* Only the named size swaps — explicit width/height are exact values
* the author already oriented. */
_paperSize() {
const named = PAPER[(this.getAttribute('size') || '').toLowerCase()] || PAPER.letter;
const landscape =
(this.getAttribute('orientation') || '').trim().toLowerCase() === 'landscape';
return landscape ? [named[1], named[0]] : named;
}
get pageWidth() {
return safeLen(this.getAttribute('width'), this._paperSize()[0]);
}
get pageHeight() {
return safeLen(this.getAttribute('height'), this._paperSize()[1]);
}
get pageMargin() { return safeLen(this.getAttribute('margin'), '0.75in'); }
/** Scaled-fit mode's content box [w, h] as CSS lengths, or null when
* the mode is off (either attribute missing/invalid/zero — a partial
* declaration falls back to normal flow rather than guessing). */
_contentFit() {
const w = safeLen(this.getAttribute('content-width'), null);
const h = safeLen(this.getAttribute('content-height'), null);
if (!w || !h) return null;
const wPx = toPx(w), hPx = toPx(h);
return wPx > 0 && hPx > 0 ? [w, h, wPx, hPx] : null;
}
connectedCallback() {
if (!this._sheet) this._render();
this._syncSize();
this._syncPrintPageRule();
this._ensureTextWrapDefaults();
this._ensureOwnsPrintMeta();
this._syncFixedSizeMeta();
if (this._mo) this._mo.observe(this, {
subtree: true, childList: true, characterData: true, attributes: true,
});
this._onResize = () => this._scheduleMeasure();
window.addEventListener('resize', this._onResize);
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => this._scheduleMeasure());
}
this._scheduleMeasure();
}
disconnectedCallback() {
window.removeEventListener('resize', this._onResize);
if (this._mo) this._mo.disconnect();
if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
// Drop the head rules when the last doc-page leaves, so a deleted
// document's @page geometry and text-wrap defaults can't apply to
// whatever replaces it.
const survivor = document.querySelector('doc-page');
if (!survivor) {
['doc-page-print', 'doc-page-text-wrap', 'doc-page-owns-print', 'doc-page-fixed-size'].forEach((id) => {
const tag = document.getElementById(id);
if (tag) tag.remove();
});
} else if (typeof survivor._syncFixedSizeMeta === 'function') {
// A departed true-size owner hands the page-global preview meta
// to whatever true-size page remains (or it's removed).
survivor._syncFixedSizeMeta();
}
}
attributeChangedCallback() {
if (!this._sheet) return;
this._syncSize();
this._syncPrintPageRule();
this._syncFixedSizeMeta();
this._scheduleMeasure();
}
_render() {
this._root.innerHTML = `
<style>${stylesheet}</style>
<style id="vars"></style>
<div class="sheet" data-screen-label="Document">
<table class="frame" role="presentation">
<thead><tr><th><div class="hdr-space"><slot name="header"></slot></div></th></tr></thead>
<tbody><tr><td class="body"><div class="fit-box"><div class="fit"><slot></slot></div></div></td></tr></tbody>
<tfoot><tr><td><div class="ftr-space"><slot name="footer"></slot></div></td></tr></tfoot>
</table>
</div>`;
this._sheet = this._root.querySelector('.sheet');
this._vars = this._root.getElementById('vars');
}
/** Runtime sizing lives in a shadow <style> :host rule, never on the
* light-DOM host element, so serialize-persist can't write it back. */
_syncSize(hdrH, ftrH) {
// Scaled-fit mode: content at its authored size, scaled onto the
// printable area (page minus margins on both axes). The factor is a
// plain number var so calc(length * number) stays valid; 4 decimals
// keeps the shadow style stable across re-measures. Upscaling is
// allowed — print transforms are vector, so text and CSS stay crisp
// (raster images soften, which the catalog bullet warns about).
const fit = this._contentFit();
let fitVars = '';
if (fit) {
const marginPx = toPx(this.pageMargin) || 0;
const availW = toPx(this.pageWidth) - 2 * marginPx;
const availH = toPx(this.pageHeight) - 2 * marginPx;
const scale = Math.min(availW / fit[2], availH / fit[3]);
if (scale > 0 && Number.isFinite(scale)) {
fitVars =
'--doc-fit-w:' + fit[0] + ';' +
'--doc-fit-h:' + fit[1] + ';' +
'--doc-fit-scale:' + scale.toFixed(4) + ';';
}
}
this._sheet.classList.toggle('fit-mode', !!fitVars);
this._vars.textContent = ':host{' +
fitVars +
'--doc-page-w:' + this.pageWidth + ';' +
'--doc-page-h:' + this.pageHeight + ';' +
'--doc-page-margin:' + this.pageMargin + ';' +
'--doc-hdr-h:' + (hdrH || 0) + 'px;' +
'--doc-ftr-h:' + (ftrH || 0) + 'px;' +
'--doc-hdr-pad:' + (hdrH ? '0.35in' : '0px') + ';' +
'--doc-ftr-pad:' + (ftrH ? '0.35in' : '0px') + '}';
}
/** @page is a no-op inside shadow DOM, so the rule lives in <head>.
* Re-appended on every sync so it stays last in source order — the
* @page cascade is source-order per descriptor, so this rule wins
* over any other @page rule in the document. */
_syncPrintPageRule() {
const id = 'doc-page-print';
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement('style');
tag.id = id;
}
document.head.appendChild(tag);
tag.textContent =
'@page { size: ' + this.pageWidth + ' ' + this.pageHeight + '; margin: 0; } ' +
'@media print { html, body { margin: 0 !important; padding: 0 !important; background: none !important; height: auto !important; overflow: visible !important; } ' +
'h1,h2,h3,h4,h5,h6 { break-after: avoid; } ' +
'figure,pre,blockquote,img,svg,tr { break-inside: avoid; } ' +
'p,li { orphans: 3; widows: 3; } ' +
'* { -webkit-print-color-adjust: exact; print-color-adjust: exact; } ' +
'*, *::before, *::after { animation-delay: -99s !important; animation-duration: .001s !important; ' +
'animation-iteration-count: 1 !important; animation-fill-mode: both !important; ' +
'animation-play-state: running !important; transition-duration: 0s !important; } }';
}
/** Typographic defaults for document text: balance headings, avoid
* widowed/orphaned words in body copy (browsers without text-wrap
* support drop the declarations). Zero-specificity via :where() so
* any text-wrap authored on those elements wins; document-level so the
* rules reach the slotted (light DOM) content — shadow styles can't.
* data-omelette-injected marks the tag for the host editor to strip
* at serialize, so it is never written back as authored source. */
_ensureTextWrapDefaults() {
if (document.getElementById('doc-page-text-wrap')) return;
const tag = document.createElement('style');
tag.id = 'doc-page-text-wrap';
tag.setAttribute('data-omelette-injected', '');
tag.textContent =
':where(h1,h2,h3,h4,h5,h6){text-wrap:balance}' +
':where(p,li,blockquote,figcaption){text-wrap:pretty}';
document.head.appendChild(tag);
}
/** Declares that this document owns its print CSS. The instant-PDF
* export checks for the meta by NAME PRESENCE alone (content is
* ignored) and skips its automatic print-CSS injections, so the
* component's @page geometry is never overridden by a heuristic.
* data-omelette-injected keeps it out of serialized source. */
_ensureOwnsPrintMeta() {
if (document.getElementById('doc-page-owns-print')) return;
const tag = document.createElement('meta');
tag.id = 'doc-page-owns-print';
tag.name = 'omelette-owns-print';
tag.content = 'true';
tag.setAttribute('data-omelette-injected', '');
document.head.appendChild(tag);
}
/** This page's valid true-size page box (explicit width AND height)
* as [w, h] px ints, or null when the mode is off. */
_trueSizePx() {
if (
!safeLen(this.getAttribute('width'), null) ||
!safeLen(this.getAttribute('height'), null)
) return null;
const w = Math.round(toPx(this.pageWidth));
const h = Math.round(toPx(this.pageHeight));
return w > 0 && h > 0 ? [w, h] : null;
}
/** True-size pages (explicit width AND height) also declare the page
* box as the preview size: the in-app preview reads
* meta[name="omelette-fixed-size"] (content "W,H" in px ints) and
* scales the sheet into view — without it an 18in poster previews at
* true size with scrollbars. Never overrides an author-set meta
* (only the component's own id is managed). The meta is page-global
* while doc-page instances are not, so every sync recomputes the
* page-wide owner — the first connected true-size doc-page — and a
* non-true-size sibling's sync can never delete the owner's meta.
* Removed when no true-size page remains (the owner's disconnect
* re-syncs via any survivor) or when an author-set meta exists. */
_syncFixedSizeMeta() {
const id = 'doc-page-fixed-size';
const own = document.getElementById(id);
const authored = document.querySelector(
'meta[name="omelette-fixed-size"]:not([data-omelette-injected])'
);
// The page-wide owner, not this instance: an upgraded true-size page
// anywhere in the document keeps the meta alive and sized.
let box = null;
for (const el of document.querySelectorAll('doc-page')) {
box = typeof el._trueSizePx === 'function' ? el._trueSizePx() : null;
if (box) break;
}
if (!box || authored) {
if (own) own.remove();
return;
}
const tag = own || document.createElement('meta');
tag.id = id;
tag.name = 'omelette-fixed-size';
tag.content = box[0] + ',' + box[1];
tag.setAttribute('data-omelette-injected', '');
if (!own) document.head.appendChild(tag);
}
_scheduleMeasure() {
if (this._raf) return;
this._raf = requestAnimationFrame(() => { this._raf = null; this._measure(); });
}
/** Slot heights feed the print spacers (--doc-hdr-h / --doc-ftr-h), so
* they re-measure on content mutation, resize, and font load. */
_measure() {
const hdr = this.querySelector(':scope > [slot="header"]');
const ftr = this.querySelector(':scope > [slot="footer"]');
this._syncSize(hdr ? hdr.offsetHeight : 0, ftr ? ftr.offsetHeight : 0);
}
}
if (!customElements.get('doc-page')) {
customElements.define('doc-page', DocPage);
}
})();