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 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.
@@ -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 > 240 || info && track.frameCount !== info.frames) throw new Error('Animation frame budget or container mismatch');
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 || w * h * state.frames > 96000000) throw new Error('Decoded animation exceeds the image budget');
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 > 120000) throw new Error('Animation exceeds the duration budget');
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
- timer = win.setTimeout(step, delay);
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(record.properties['font-family'], bytes, descriptor).load(); }
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 margin = settings?.geometry ? settings.geometry.twips / 20 : null;
184
- const margins = {top: margin ?? 12 * 72 / 25.4, right: margin ?? 14 * 72 / 25.4,
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 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
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
- for (let first = 0; first < count;) {
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 picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
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\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
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')}{--md-color-line:#000000!important}
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
- if (theme === 'system') for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g))
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
  }
@@ -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 templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot);
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 printMargin = settings?.geometry ? settings.geometry.twips / 15 : 14 * 96 / 25.4;
142
- const printWidth = settings?.papersize ? settings.papersize.width / 15 - 2 * printMargin : null;
143
- const typographyPrintCss = refined ? '@media print{.md-render{--rapier-print-margin-left:' + printMargin + 'px;--rapier-print-margin-right:' + printMargin + 'px'
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, margin: templatePage.margin},
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, margin = (settings.geometry?.twips ?? 1440) / 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:#e8e8e8}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + margin
219
- + 'px;background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,#fff;color:#111}}\n'
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:' + margin + 'px!important;position:relative!important;z-index:1;box-decoration-break:clone;-webkit-box-decoration-break:clone}'
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, margin, css};
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'; into.body.appendChild(surface);
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 match = /^margin\s*=\s*(\d+(?:\.\d+)?)(cm|mm|in|pt)$/i.exec(unquote(value));
135
- if (!match) return null;
136
- const number = Number(match[1]);
137
- if (!Number.isFinite(number) || number < 0) return null;
138
- const unit = match[2].toLowerCase();
139
- const twips = Math.round(unit === 'in' ? number * 1440 : unit === 'pt' ? number * 20 : unit === 'cm' ? number * 1440 / 2.54 : number * 1440 / 25.4);
140
- if (!Number.isSafeInteger(twips) || twips < 0 || twips > 1440 * 6) return null;
141
- return Object.freeze({margin: match[1] + unit, twips});
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 = PAPERS[String(take('papersize') || '').toLowerCase()] || null;
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.18",
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",