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 +1 -1
- package/dist/kit/export-fonts.mjs +1 -1
- package/dist/kit/render-print.mjs +2 -3
- package/dist/kit/render-styles.mjs +6 -4
- package/dist/kit/render.mjs +34 -15
- package/dist/spec/document-settings.mjs +19 -12
- 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.
|
|
@@ -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,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
|
|
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
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')}{
|
|
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.');
|
|
@@ -131,14 +131,21 @@ function unquote(value) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function marginOf(value) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
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",
|