rapier-markdown-kit 1.2.18 → 1.2.19

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.
@@ -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,9 +180,8 @@ 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
187
  if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
@@ -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.');
@@ -131,14 +131,21 @@ function unquote(value) {
131
131
  }
132
132
 
133
133
  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});
134
+ const lengths = new Map();
135
+ for (const part of unquote(value).split(',')) {
136
+ const match = /^\s*(margin|top|right|bottom|left)\s*=\s*(\d+(?:\.\d+)?)(cm|mm|in|pt)\s*$/i.exec(part);
137
+ if (!match) return null;
138
+ const key = match[1].toLowerCase(), number = Number(match[2]), unit = match[3].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 (lengths.has(key) || !Number.isSafeInteger(twips) || twips < 0 || twips > 1440 * 6) return null;
141
+ lengths.set(key, {css: match[2] + unit, twips});
142
+ }
143
+ const sides = ['top', 'right', 'bottom', 'left'].map(key => lengths.get(key) || lengths.get('margin'));
144
+ if (sides.some(side => !side)) return null;
145
+ const [top, right, bottom, left] = sides.map(side => side.twips);
146
+ const uniform = sides.every(side => side.twips === top);
147
+ return Object.freeze({margin: uniform ? sides[0].css : sides.map(side => side.css).join(' '),
148
+ twips: uniform ? top : null, top, right, bottom, left});
142
149
  }
143
150
 
144
151
  function quotedEnd(text, start = 0, limit = text.length) {
@@ -352,14 +359,14 @@ export function updateDocumentSettings(markdown, patch) {
352
359
  text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
353
360
  for (const [key, supplied] of Object.entries(patch || {})) {
354
361
  const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
355
- if (!font && !['fontsize', 'linestretch', 'spacing'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
362
+ if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
356
363
  const matches = fields.filter(item => item.key === key);
357
364
  if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
358
365
  const field = matches[0];
359
- const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : leading(supplied);
366
+ const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : leading(supplied);
360
367
  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;
368
+ const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : String(value.factor);
369
+ 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]) : before.linestretch.factor === value.factor)) continue;
363
370
  if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
364
371
  if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
365
372
  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.19",
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",