rapier-markdown-kit 1.2.18 → 1.2.20
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/README.md +1 -1
- package/dist/images/playback.mjs +9 -6
- package/dist/kit/export-fonts.mjs +1 -1
- package/dist/kit/render-print.mjs +24 -8
- package/dist/kit/render-styles.mjs +6 -4
- package/dist/kit/render.mjs +34 -15
- package/dist/spec/document-settings.mjs +29 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
|
|
|
59
59
|
Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
|
|
60
60
|
as one HTML file on Android, Web and Windows.
|
|
61
61
|
|
|
62
|
-
1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `open` to share one document with the person.
|
|
62
|
+
1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
|
|
63
63
|
2. **Deliver an offline editor.** Run `npx rapier-html notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
|
|
64
64
|
3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
|
|
65
65
|
4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
|
package/dist/images/playback.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
export function createMediaPlayback(win, inspectGIF) {
|
|
3
|
+
const FRAMES = 3600, LOOP_MS = 3600000;
|
|
3
4
|
const active = new Map(), encode = bytes => bytes.toBase64();
|
|
4
5
|
const bytes = async url => {
|
|
5
6
|
const text = url.slice(url.indexOf(',') + 1), slice = 1 << 20;
|
|
@@ -14,7 +15,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
14
15
|
function raster(url, canvas, changed = () => {}, dimensions = null) {
|
|
15
16
|
if (url.length > 24 * 1024 * 1024 || !/^data:image\/(gif|jxl);base64,/i.test(url) || (url.length - url.indexOf(',') - 1) % 4) throw new Error('Unsupported animation source');
|
|
16
17
|
const type = url.slice(5, url.indexOf(';')).toLowerCase();
|
|
17
|
-
let data = null, info = null, size = null, decoder = null, timer = 0, closed = false, paused = true, ticket = 0, next = 0, loops = 0, duration = 0, single = false, held = false;
|
|
18
|
+
let data = null, info = null, size = null, decoder = null, timer = 0, closed = false, paused = true, ticket = 0, next = 0, loops = 0, duration = 0, single = false, held = false, due = 0;
|
|
18
19
|
const state = {animated: true, status: 'loading', first: '', frames: 0};
|
|
19
20
|
const admitted = (async () => {
|
|
20
21
|
const buffer = await bytes(url); info = type === 'image/gif' ? inspectGIF(buffer) : null;
|
|
@@ -47,11 +48,12 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
47
48
|
state.status = 'playback-budget'; paused = true; changed(state); return;
|
|
48
49
|
}
|
|
49
50
|
active.set(canvas, size.width * size.height);
|
|
51
|
+
next = 0; duration = 0; due = 0;
|
|
50
52
|
decoder = new win.ImageDecoder({data, type, preferAnimation: true});
|
|
51
53
|
const current = decoder; await settle(current.tracks.ready);
|
|
52
54
|
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
53
55
|
const track = current.tracks.selectedTrack;
|
|
54
|
-
if (!track || track.frameCount < 1 || track.frameCount >
|
|
56
|
+
if (!track || track.frameCount < 1 || track.frameCount > FRAMES || info && track.frameCount !== info.frames) throw new Error('Animation frame budget or container mismatch');
|
|
55
57
|
state.frames = track.frameCount; state.animated = track.animated;
|
|
56
58
|
}
|
|
57
59
|
const current = decoder, decoding = current.decode({frameIndex: next, completeFramesOnly: true});
|
|
@@ -62,13 +64,13 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
62
64
|
try {
|
|
63
65
|
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
64
66
|
const w = frame.displayWidth, h = frame.displayHeight;
|
|
65
|
-
if (!result.complete || w !== size.width || h !== size.height
|
|
67
|
+
if (!result.complete || w !== size.width || h !== size.height) throw new Error('Decoded animation exceeds the image budget');
|
|
66
68
|
if (canvas.width !== w) canvas.width = w;
|
|
67
69
|
if (canvas.height !== h) canvas.height = h;
|
|
68
70
|
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
69
71
|
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
70
72
|
const delay = type !== 'image/gif' ? Math.max(8, frame.duration / 1000) : frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
71
|
-
duration += delay; if (duration >
|
|
73
|
+
duration += delay; if (duration > LOOP_MS) throw new Error('Animation exceeds the duration budget');
|
|
72
74
|
state.status = 'ready'; state.frame = next; changed(state);
|
|
73
75
|
if (single) {paused = true; closeDecoder(); return;}
|
|
74
76
|
next++;
|
|
@@ -76,7 +78,8 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
76
78
|
next = 0; duration = 0; loops++;
|
|
77
79
|
if (!state.animated || Number.isFinite(current.tracks.selectedTrack.repetitionCount) && loops > current.tracks.selectedTrack.repetitionCount) {paused = true; closeDecoder(); return;}
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
const now = win.performance.now(); due = due && now - due < 1000 ? due + delay : now + delay;
|
|
82
|
+
timer = win.setTimeout(step, Math.max(0, due - now));
|
|
80
83
|
} finally {frame.close();}
|
|
81
84
|
} catch (error) {
|
|
82
85
|
if (closed || stamp !== ticket) return;
|
|
@@ -85,7 +88,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
85
88
|
}
|
|
86
89
|
return {state, play() {if (!closed && paused && !['decode-failed', 'admission-failed'].includes(state.status)) {paused = false; void step();}},
|
|
87
90
|
async first() {if (!state.first && !closed) {single = true; next = 0; paused = false; try {await step();} finally {single = false; paused = true; closeDecoder(); if (held) {held = false; this.suspend();}}} return state.first;},
|
|
88
|
-
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0;},
|
|
91
|
+
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0; due = 0;},
|
|
89
92
|
suspend() {if (single) {held = true; return;} paused = true; closeDecoder(); canvas.width = canvas.height = 0; state.status = 'suspended'; changed(state);},
|
|
90
93
|
close() {closed = true; paused = true; closeDecoder(); canvas.width = canvas.height = 0;},
|
|
91
94
|
get paused() {return paused;}};
|
|
@@ -403,7 +403,7 @@ export async function waitForFonts(root, {settings, css = '', fontCss = '', inst
|
|
|
403
403
|
for (const [css, name] of [['font-style', 'style'], ['font-weight', 'weight'], ['font-stretch', 'stretch'], ['unicode-range', 'unicodeRange']])
|
|
404
404
|
if (record.properties[css]) descriptor[name] = record.properties[css];
|
|
405
405
|
const bytes = Uint8Array.from(atob(record.source.slice(record.source.indexOf(',') + 1)), character => character.charCodeAt(0));
|
|
406
|
-
try { return await new Font(
|
|
406
|
+
try { return await new Font(family, bytes, descriptor).load(); }
|
|
407
407
|
catch (cause) { throw Object.assign(new Error('The embedded font could not be loaded: ' + family, {cause}), {code: 'export_font_unavailable'}); }
|
|
408
408
|
}));
|
|
409
409
|
if (install) for (const face of loaded) doc.fonts.add(face);
|
|
@@ -180,12 +180,12 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
180
180
|
if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
|
|
181
181
|
const paper = settings?.papersize || {width: 11906, height: 16838};
|
|
182
182
|
const width = paper.width / 20, height = paper.height / 20;
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
bottom: margin ?? 14 * 72 / 25.4, left: margin ?? 14 * 72 / 25.4};
|
|
183
|
+
const margins = Object.fromEntries(['top', 'right', 'bottom', 'left'].map(side =>
|
|
184
|
+
[side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
|
|
186
185
|
const bodyWidth = Math.floor((width - margins.left - margins.right) * 4 / 3);
|
|
187
186
|
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
188
|
-
if (bodyWidth < 1 || bodyHeight < 1
|
|
187
|
+
if (bodyWidth < 1 || bodyHeight < 1) refuse('pdf_page_geometry_invalid');
|
|
188
|
+
const tiled = bodyWidth > CAPTURE_EDGE || bodyHeight > CAPTURE_EDGE || bodyWidth * bodyHeight > CAPTURE_PIXELS;
|
|
189
189
|
const frame = document.createElement('iframe');
|
|
190
190
|
let vectors, backdrop = null, nativeGlyphs, filmSession;
|
|
191
191
|
let rejectAbort;
|
|
@@ -320,7 +320,21 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
320
320
|
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
321
321
|
pages.push({rgb: packed});
|
|
322
322
|
};
|
|
323
|
-
|
|
323
|
+
const tileWidth = Math.min(bodyWidth, CAPTURE_EDGE), tileHeight = Math.min(bodyHeight, CAPTURE_EDGE, Math.floor(CAPTURE_PIXELS / tileWidth));
|
|
324
|
+
for (let index = 0; tiled && index < count; index++) {
|
|
325
|
+
const tiles = [];
|
|
326
|
+
for (let y = 0; y < bodyHeight; y += tileHeight) for (let x = 0; x < bodyWidth; x += tileWidth) {
|
|
327
|
+
check();
|
|
328
|
+
const width = Math.min(tileWidth, bodyWidth - x), height = Math.min(tileHeight, bodyHeight - y);
|
|
329
|
+
const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}));
|
|
330
|
+
if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
331
|
+
await pack(shot.rgb);
|
|
332
|
+
shot.rgb = null;
|
|
333
|
+
tiles.push({...pages.pop(), x, y, width, height});
|
|
334
|
+
}
|
|
335
|
+
pages.push({tiles});
|
|
336
|
+
}
|
|
337
|
+
for (let first = 0; !tiled && first < count;) {
|
|
324
338
|
check();
|
|
325
339
|
const span = Math.min(share, count - first);
|
|
326
340
|
try {
|
|
@@ -699,9 +713,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
699
713
|
const marked = [];
|
|
700
714
|
parentTree.push(marked);
|
|
701
715
|
const held = pages[index], flate = !!held?.rgb;
|
|
702
|
-
const
|
|
716
|
+
const tiles = held?.tiles || [{...(flate ? held : {jpeg: held}), x: 0, y: 0, width: pixelWidth, height: pixelHeight}];
|
|
717
|
+
const pictures = tiles.map((tile, at) => '/Im0_' + at + ' ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
|
|
718
|
+
' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (tile.rgb ? 'FlateDecode' : 'DCTDecode'), tile.rgb || tile.jpeg) + ' 0 R'), states = [];
|
|
703
719
|
pages[index] = null;
|
|
704
|
-
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
705
720
|
const emit = (drawing, left, top, clipWidth, clipHeight) => {
|
|
706
721
|
let out = `q\n.75 0 0 -.75 ${decimal(left)} ${decimal(top)} cm\n0 0 ${clipWidth} ${clipHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
707
722
|
out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
@@ -729,7 +744,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
729
744
|
};
|
|
730
745
|
let commands = '';
|
|
731
746
|
for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
|
|
732
|
-
commands += '/Artifact BMC\
|
|
747
|
+
commands += '/Artifact BMC\n' + tiles.map((tile, at) => 'q\n' + [decimal(tile.width * .75), 0, 0, decimal(tile.height * .75), decimal(margins.left + tile.x * .75),
|
|
748
|
+
decimal(height - margins.top - (tile.y + tile.height) * .75)].join(' ') + ' cm\n/Im0_' + at + ' Do\nQ\n').join('') + 'EMC\n';
|
|
733
749
|
for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
734
750
|
const textAlphas = new Map();
|
|
735
751
|
for (const run of textPages[index]) {
|
|
@@ -33,7 +33,7 @@ function _rapierArtifactPrintCss() {
|
|
|
33
33
|
html,body{
|
|
34
34
|
width:auto!important;height:auto!important;min-height:0!important;max-height:none!important;
|
|
35
35
|
margin:0!important;padding:0!important;overflow:visible!important;position:static!important;
|
|
36
|
-
background:#fff!important;color:#111!important;color-scheme:light
|
|
36
|
+
background:#fff!important;color:#111!important;color-scheme:light!important
|
|
37
37
|
}
|
|
38
38
|
.rapier-page{
|
|
39
39
|
display:block!important;width:100%!important;max-width:none!important;height:auto!important;
|
|
@@ -49,7 +49,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
49
49
|
return `
|
|
50
50
|
@page{size:auto;margin:12mm 14mm 14mm}
|
|
51
51
|
@media print{
|
|
52
|
-
${p('.md-render')}{
|
|
52
|
+
${p('.md-render')}{color-scheme:light!important;--md-color-line:#000000!important}
|
|
53
53
|
${list('.md-render h1','.md-render h2','.md-render h3','.md-render h4','.md-render h5','.md-render h6')}{page-break-after:avoid;break-after:avoid-page;color:#111}
|
|
54
54
|
${list('.md-render p','.md-render li','.md-render blockquote')}{orphans:3;widows:3}
|
|
55
55
|
${p('.md-render img')}{max-width:100%!important;height:auto!important;break-inside:avoid-page}
|
|
@@ -97,8 +97,10 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
|
|
|
97
97
|
:root,.md-render{--md-font-sans:'Geist','Geist/Roboto','Geist/Helvetica Neue','Geist/Segoe UI','Geist/Arial','Geist/Noto Sans','Geist/Ubuntu','Geist/Liberation Sans','Geist/DejaVu Sans',system-ui,sans-serif}`);
|
|
98
98
|
const highlightCss = _rapierStyleText('rapier-highlight-style');
|
|
99
99
|
parts.push(highlightCss);
|
|
100
|
-
|
|
101
|
-
parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
|
|
100
|
+
for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g)) {
|
|
101
|
+
if (theme === 'system') parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
|
|
102
|
+
if (printMode) parts.push('@media print{html[data-highlights="' + palette + '"] body{' + light.trim() + '}}');
|
|
103
|
+
}
|
|
102
104
|
if (printMode) parts.push(_rapierArtifactPrintCss());
|
|
103
105
|
return parts.filter(Boolean).join('\n\n');
|
|
104
106
|
}
|
package/dist/kit/render.mjs
CHANGED
|
@@ -48,6 +48,9 @@ function createRenderer(runtime) {
|
|
|
48
48
|
async function _rapierBuildArtifact(options, providedContext) {
|
|
49
49
|
const opts = options || {};
|
|
50
50
|
const context = providedContext || _rapierBuildInterchangeContext(opts);
|
|
51
|
+
const preference = _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
|
|
52
|
+
const theme = opts.print ? 'light' : preference === 'system'
|
|
53
|
+
? (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : preference;
|
|
51
54
|
const metadata = context.metadata;
|
|
52
55
|
const base = context.baseName;
|
|
53
56
|
const work = context.work || opts.work;
|
|
@@ -110,7 +113,8 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
110
113
|
const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
|
|
111
114
|
: _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
|
|
112
115
|
styledRoot.setAttribute('lang', language);
|
|
113
|
-
const
|
|
116
|
+
const darkPaper = theme === 'dark' && _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false';
|
|
117
|
+
const templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot, darkPaper);
|
|
114
118
|
if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
|
|
115
119
|
if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
116
120
|
if (completePage || opts.print) {
|
|
@@ -123,9 +127,6 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
123
127
|
}
|
|
124
128
|
if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
|
|
125
129
|
markPrintCapitals(styledRoot);
|
|
126
|
-
const theme = opts.print || templatePage
|
|
127
|
-
? 'light'
|
|
128
|
-
: _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
|
|
129
130
|
const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
|
|
130
131
|
const invertImageAlpha = _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false';
|
|
131
132
|
const versionMeta = document.querySelector('meta[name="rapier-version"]');
|
|
@@ -138,9 +139,10 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
138
139
|
const willFont = opts.print && opts.willFont ? opts.willFont : null;
|
|
139
140
|
const settingsCss = _rapierDocumentSettingsCss(settings);
|
|
140
141
|
const refined = styledRoot.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"],img[data-rapier-print-lines]');
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
142
|
+
const printLeft = settings?.geometry ? settings.geometry.left / 15 : 14 * 96 / 25.4;
|
|
143
|
+
const printRight = settings?.geometry ? settings.geometry.right / 15 : 14 * 96 / 25.4;
|
|
144
|
+
const printWidth = settings?.papersize ? settings.papersize.width / 15 - printLeft - printRight : null;
|
|
145
|
+
const typographyPrintCss = refined ? '@media print{.md-render{--rapier-print-margin-left:' + printLeft + 'px;--rapier-print-margin-right:' + printRight + 'px'
|
|
144
146
|
+ (printWidth > 0 ? ';--rapier-print-width:' + printWidth + 'px' : '') + '}'
|
|
145
147
|
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
146
148
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
@@ -194,7 +196,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
194
196
|
fontCss,
|
|
195
197
|
fontRequests: fontCss ? fontRequests(styledRoot, {settings, css, fontCss}) : [],
|
|
196
198
|
settings,
|
|
197
|
-
templatePage: templatePage && {url: templatePage.url, width: templatePage.width, height: templatePage.height,
|
|
199
|
+
templatePage: templatePage && {url: templatePage.url, width: templatePage.width, height: templatePage.height, margins: templatePage.margins},
|
|
198
200
|
docClass,
|
|
199
201
|
language,
|
|
200
202
|
metadata,
|
|
@@ -202,7 +204,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
202
204
|
};
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
function _rapierTemplatePage(canonical, settings, root) {
|
|
207
|
+
function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
|
|
206
208
|
if (!isTemplatePage(canonical) || !settings?.papersize) return null;
|
|
207
209
|
let url = '';
|
|
208
210
|
for (const paragraph of root.querySelectorAll(':scope > p[data-md-layout]')) {
|
|
@@ -212,15 +214,31 @@ function _rapierTemplatePage(canonical, settings, root) {
|
|
|
212
214
|
if (isPageBorderLayout(layout) && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+$/.test(source)) { url = source; paragraph.remove(); break; }
|
|
213
215
|
}
|
|
214
216
|
if (!url) return null;
|
|
215
|
-
const width = settings.papersize.width / 15, height = settings.papersize.height / 15
|
|
217
|
+
const width = settings.papersize.width / 15, height = settings.papersize.height / 15;
|
|
218
|
+
const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings.geometry?.[side] ?? 1440) / 15);
|
|
219
|
+
const padding = margins.map(value => value + 'px').join(' ');
|
|
216
220
|
const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
|
|
221
|
+
const shown = darkPaper ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
|
|
217
222
|
const css = ':root{--rapier-template-border:url("' + url + '")}\n'
|
|
218
|
-
+ '@media screen{.rapier-template-border{display:none}body{background
|
|
219
|
-
+ '
|
|
223
|
+
+ '@media screen{' + (shown !== url ? ':root{--rapier-template-border:url("' + shown + '")}' : '') + '.rapier-template-border{display:none}body{background:var(--md-color-inset)}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + padding
|
|
224
|
+
+ ';background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,var(--md-color-bg);color:var(--md-color-text)}}\n'
|
|
220
225
|
+ '@media print{@page{size:' + width + 'px ' + height + 'px;margin:0}html,body{margin:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}'
|
|
221
|
-
+ '.rapier-page{box-sizing:border-box;width:auto;max-width:none;margin:0;padding:' +
|
|
226
|
+
+ '.rapier-page{box-sizing:border-box;width:auto;max-width:none;margin:0;padding:' + padding + '!important;position:relative!important;z-index:1;box-decoration-break:clone;-webkit-box-decoration-break:clone}'
|
|
222
227
|
+ '.rapier-template-border{display:block;position:fixed;left:0;top:0;width:' + width + 'px;height:' + height + 'px;z-index:0;' + ground + '}}';
|
|
223
|
-
return {url, width, height,
|
|
228
|
+
return {url, width, height, margins, css};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _rapierDarkPaperSvgUrl(url, paperInk) {
|
|
232
|
+
const ink = globalThis.RapierImageInspection?.inkForPaper;
|
|
233
|
+
if (typeof ink !== 'function') return url;
|
|
234
|
+
let text;
|
|
235
|
+
try { text = new TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(atob(url.slice(26)), c => c.charCodeAt(0))); }
|
|
236
|
+
catch (_) { return url; }
|
|
237
|
+
const inked = ink(text, {dark: true, paperInk});
|
|
238
|
+
if (inked === text) return url;
|
|
239
|
+
let binary = '';
|
|
240
|
+
for (const byte of new TextEncoder().encode(inked)) binary += String.fromCharCode(byte);
|
|
241
|
+
return 'data:image/svg+xml;base64,' + btoa(binary);
|
|
224
242
|
}
|
|
225
243
|
|
|
226
244
|
function _rapierPagePreviewMeta(root, fallbackTitle) {
|
|
@@ -346,7 +364,8 @@ function _rapierDocxRenderStyles(root) {
|
|
|
346
364
|
const sheet = into.createElement('style');
|
|
347
365
|
sheet.textContent = _rapierArtifactStyles('light', false, false);
|
|
348
366
|
into.head.appendChild(sheet);
|
|
349
|
-
const surface = into.createElement('div'); surface.className = 'md-render';
|
|
367
|
+
const surface = into.createElement('div'); surface.className = 'md-render';
|
|
368
|
+
surface.setAttribute('data-md-theme', 'light'); into.body.appendChild(surface);
|
|
350
369
|
const canvas = into.createElement('canvas'); canvas.width = canvas.height = 1;
|
|
351
370
|
const paint = canvas.getContext('2d'), colours = new Map();
|
|
352
371
|
if (!paint) throw new Error('Word text colors could not be read.');
|
|
@@ -119,7 +119,16 @@ const PAPERS = Object.freeze({
|
|
|
119
119
|
a4: Object.freeze({name: 'a4', width: 11906, height: 16838, css: 'A4'}),
|
|
120
120
|
a5: Object.freeze({name: 'a5', width: 8391, height: 11906, css: 'A5'}),
|
|
121
121
|
legal: Object.freeze({name: 'legal', width: 12240, height: 20160, css: 'legal'}),
|
|
122
|
+
tabloid: Object.freeze({name: 'tabloid', width: 15840, height: 24480, css: '11in 17in'}),
|
|
123
|
+
a3: Object.freeze({name: 'a3', width: 16838, height: 23811, css: 'A3'}),
|
|
122
124
|
});
|
|
125
|
+
function paperOf(value) {
|
|
126
|
+
const name = String(value || '').toLowerCase(), named = Object.hasOwn(PAPERS, name) ? PAPERS[name] : null;
|
|
127
|
+
if (named) return named;
|
|
128
|
+
const pixels = /^(\d{3,5})x(\d{3,5})px$/.exec(name), width = Number(pixels?.[1]), height = Number(pixels?.[2]);
|
|
129
|
+
return pixels && width <= 10000 && height <= 10000 ? Object.freeze({name, width: width * 15, height: height * 15, css: width + 'px ' + height + 'px'}) : null;
|
|
130
|
+
}
|
|
131
|
+
export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
|
|
123
132
|
const EMPTY = Object.freeze({
|
|
124
133
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
125
134
|
papersize: null, geometry: null, pagestyle: null, spacing: null,
|
|
@@ -131,14 +140,21 @@ function unquote(value) {
|
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
function marginOf(value) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
const lengths = new Map();
|
|
144
|
+
for (const part of unquote(value).split(',')) {
|
|
145
|
+
const match = /^\s*(margin|top|right|bottom|left)\s*=\s*(\d+(?:\.\d+)?)(cm|mm|in|pt)\s*$/i.exec(part);
|
|
146
|
+
if (!match) return null;
|
|
147
|
+
const key = match[1].toLowerCase(), number = Number(match[2]), unit = match[3].toLowerCase();
|
|
148
|
+
const twips = Math.round(unit === 'in' ? number * 1440 : unit === 'pt' ? number * 20 : unit === 'cm' ? number * 1440 / 2.54 : number * 1440 / 25.4);
|
|
149
|
+
if (lengths.has(key) || !Number.isSafeInteger(twips) || twips < 0 || twips > 1440 * 6) return null;
|
|
150
|
+
lengths.set(key, {css: match[2] + unit, twips});
|
|
151
|
+
}
|
|
152
|
+
const sides = ['top', 'right', 'bottom', 'left'].map(key => lengths.get(key) || lengths.get('margin'));
|
|
153
|
+
if (sides.some(side => !side)) return null;
|
|
154
|
+
const [top, right, bottom, left] = sides.map(side => side.twips);
|
|
155
|
+
const uniform = sides.every(side => side.twips === top);
|
|
156
|
+
return Object.freeze({margin: uniform ? sides[0].css : sides.map(side => side.css).join(' '),
|
|
157
|
+
twips: uniform ? top : null, top, right, bottom, left});
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
function quotedEnd(text, start = 0, limit = text.length) {
|
|
@@ -327,7 +343,7 @@ export function readDocumentSettings(markdown) {
|
|
|
327
343
|
const monofont = documentFont(take('monofont'));
|
|
328
344
|
const defaultfont = documentFont(take('defaultfont'));
|
|
329
345
|
const linestretch = leading(take('linestretch'));
|
|
330
|
-
const papersize =
|
|
346
|
+
const papersize = paperOf(take('papersize'));
|
|
331
347
|
const geometry = take('geometry') != null ? marginOf(take('geometry')) : null;
|
|
332
348
|
const page = String(take('pagestyle') || '').toLowerCase();
|
|
333
349
|
return Object.freeze({
|
|
@@ -352,14 +368,14 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
352
368
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
353
369
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
354
370
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
355
|
-
if (!font && !['fontsize', 'linestretch', 'spacing'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
371
|
+
if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
356
372
|
const matches = fields.filter(item => item.key === key);
|
|
357
373
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
358
374
|
const field = matches[0];
|
|
359
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : leading(supplied);
|
|
375
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : leading(supplied);
|
|
360
376
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
361
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : String(value.factor);
|
|
362
|
-
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : before.linestretch.factor === value.factor)) continue;
|
|
377
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' ? value.name : String(value.factor);
|
|
378
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : before.linestretch.factor === value.factor)) continue;
|
|
363
379
|
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
364
380
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
365
381
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.20",
|
|
4
4
|
"description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|