rapier-markdown-kit 1.2.9 → 1.2.10

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.
@@ -171,7 +171,7 @@ function _rapierPrintWillPageCss() {
171
171
 
172
172
  const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
173
173
 
174
- async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
174
+ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
175
175
  const refuse = code => { throw Object.assign(new Error(code), {code}); };
176
176
  const check = () => {
177
177
  if (signal?.aborted) refuse('cancelled');
@@ -187,7 +187,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
187
187
  const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
188
188
  if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
189
189
  const frame = document.createElement('iframe');
190
- let vectors, nativeGlyphs, filmSession;
190
+ let vectors, backdrop = null, nativeGlyphs, filmSession;
191
191
  let rejectAbort;
192
192
  const aborted = new Promise((_, reject) => { rejectAbort = reject; });
193
193
  aborted.catch(() => {});
@@ -233,6 +233,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
233
233
  strip.append(root); surface.append(strip); into.body.append(surface);
234
234
  const main = root.querySelector('main');
235
235
  if (!main) refuse('pdf_render_unavailable');
236
+ if (templatePage) for (const image of main.querySelectorAll('img')) if (image.getAttribute('src') === templatePage.url) (image.closest('p,figure') || image).remove();
236
237
  main.setAttribute('data-md-theme', 'light');
237
238
  for (const details of main.querySelectorAll('details')) details.open = true;
238
239
  main.getBoundingClientRect();
@@ -255,7 +256,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
255
256
  }
256
257
  for (const [name, value] of Object.entries({width: bodyWidth + 'px', height: bodyHeight + 'px',
257
258
  'max-width': 'none', 'min-height': '0', padding: '0', margin: '0', overflow: 'visible',
258
- 'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
259
+ 'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto', background: 'none'}))
259
260
  main.style.setProperty(name, value, 'important');
260
261
  globalThis.RapierMath?.fit(main);
261
262
  applyPrintCapitals(main);
@@ -334,11 +335,22 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
334
335
  first += span;
335
336
  }
336
337
  check();
337
- const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins,
338
+ if (templatePage) {
339
+ const sheetWidth = Math.round(width * 4 / 3), sheetHeight = Math.round(height * 4 / 3);
340
+ const holder = into.createElement('div'), picture = into.createElement('img');
341
+ holder.style.cssText = `position:fixed;left:0;top:0;width:${sheetWidth}px;height:${sheetHeight}px;pointer-events:none`;
342
+ picture.style.cssText = 'display:block;width:100%;height:100%'; picture.src = templatePage.url;
343
+ holder.append(picture); into.body.append(holder);
344
+ await step(picture.decode());
345
+ backdrop = await collectPdfVectors(holder, {width: sheetWidth, height: sheetHeight, count: 1, capture, signal, current, check, maxBytes,
346
+ sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
347
+ if (!backdrop.pages[0].length) refuse('pdf_template_border_unavailable');
348
+ }
349
+ const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
338
350
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
339
351
  check();
340
352
  return {bytes, pages: count};
341
- } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
353
+ } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); backdrop?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
342
354
  }
343
355
 
344
356
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
@@ -481,7 +493,7 @@ function _rapierPrintSlots(text, rtl) {
481
493
  return {slots, count};
482
494
  }
483
495
 
484
- async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
496
+ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], backdrop = null, canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
485
497
  const encoder = new TextEncoder(), encode = value => encoder.encode(value);
486
498
  const fail = code => { throw Object.assign(new Error(code), {code}); };
487
499
  const concat = parts => {
@@ -565,11 +577,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
565
577
  const held = pages[index], flate = !!held?.rgb;
566
578
  const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
567
579
  pages[index] = null;
568
- let 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';
569
580
  const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
570
- for (const drawing of vectorPages[index] || []) {
571
- commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
572
- commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
581
+ const emit = (drawing, left, top, clipWidth, clipHeight) => {
582
+ 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`;
583
+ out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
573
584
  for (const piece of drawing.sequence) {
574
585
  if (piece.image) {
575
586
  let shared = sharedImages.get(piece.image);
@@ -583,15 +594,19 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
583
594
  sharedImages.set(piece.image, shared);
584
595
  }
585
596
  if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
586
- commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
597
+ out += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
587
598
  } else {
588
599
  const state = '/Gs' + states.length;
589
600
  states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
590
- commands += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
601
+ out += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
591
602
  }
592
603
  }
593
- commands += 'Q\n';
594
- }
604
+ return out + 'Q\n';
605
+ };
606
+ let commands = '';
607
+ for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
608
+ 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';
609
+ for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
595
610
  const textAlphas = new Map();
596
611
  for (const run of textPages[index]) {
597
612
  if (!run.text) continue;
@@ -8,6 +8,7 @@ import {documentLanguage, documentSettingsFonts} from '../spec/document-settings
8
8
  import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
9
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
10
10
  import {freezePublishedInk} from './render-ink.mjs';
11
+ import {isTemplatePage, isPageBorderLayout} from '../spec/template-page.mjs';
11
12
  export function positionNativeDiagram(drawing) {
12
13
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
13
14
  if(!surface?.clientWidth)return;
@@ -109,6 +110,7 @@ async function _rapierBuildArtifact(options, providedContext) {
109
110
  const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
110
111
  : _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
111
112
  styledRoot.setAttribute('lang', language);
113
+ const templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot);
112
114
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
113
115
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
114
116
  if (completePage || opts.print) {
@@ -121,7 +123,7 @@ async function _rapierBuildArtifact(options, providedContext) {
121
123
  }
122
124
  if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
123
125
  markPrintCapitals(styledRoot);
124
- const theme = opts.print
126
+ const theme = opts.print || templatePage
125
127
  ? 'light'
126
128
  : _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
127
129
  const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
@@ -147,7 +149,8 @@ async function _rapierBuildArtifact(options, providedContext) {
147
149
  + (opts.extraCss ? '\n\n' + opts.extraCss : '')
148
150
  + (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
149
151
  + (settingsCss ? '\n' + settingsCss : '')
150
- + (typographyPrintCss ? '\n' + typographyPrintCss : '');
152
+ + (typographyPrintCss ? '\n' + typographyPrintCss : '')
153
+ + (templatePage ? '\n' + templatePage.css : '');
151
154
  let factoryFontCss = '';
152
155
  if (!opts.print) {
153
156
  if (fontIndex) await requireDocumentFonts(context.canonical, {parser: md, index: fontIndex, root: styledRoot, settings, css, fontCss, validateFont: runtime.validateFont});
@@ -180,7 +183,7 @@ async function _rapierBuildArtifact(options, providedContext) {
180
183
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
181
184
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
182
185
  + '<style>' + css + '</style>\n</head>\n'
183
- + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
186
+ + '<body class="' + bodyClass + '">' + (templatePage ? '<div class="rapier-template-border" aria-hidden="true"></div>' : '') + '<main class="' + docClass + '" data-md-theme="' + theme + '">'
184
187
  + bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
185
188
 
186
189
  return {
@@ -191,6 +194,7 @@ async function _rapierBuildArtifact(options, providedContext) {
191
194
  fontCss,
192
195
  fontRequests: fontCss ? fontRequests(styledRoot, {settings, css, fontCss}) : [],
193
196
  settings,
197
+ templatePage: templatePage && {url: templatePage.url, width: templatePage.width, height: templatePage.height, margin: templatePage.margin},
194
198
  docClass,
195
199
  language,
196
200
  metadata,
@@ -198,6 +202,27 @@ async function _rapierBuildArtifact(options, providedContext) {
198
202
  };
199
203
  }
200
204
 
205
+ function _rapierTemplatePage(canonical, settings, root) {
206
+ if (!isTemplatePage(canonical) || !settings?.papersize) return null;
207
+ let url = '';
208
+ for (const paragraph of root.querySelectorAll(':scope > p[data-md-layout]')) {
209
+ const image = paragraph.querySelector('img');
210
+ const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(paragraph.getAttribute('data-md-layout'));
211
+ const source = image?.getAttribute('src') || '';
212
+ if (isPageBorderLayout(layout) && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+$/.test(source)) { url = source; paragraph.remove(); break; }
213
+ }
214
+ if (!url) return null;
215
+ const width = settings.papersize.width / 15, height = settings.papersize.height / 15, margin = (settings.geometry?.twips ?? 1440) / 15;
216
+ const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
217
+ 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'
220
+ + '@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}'
222
+ + '.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};
224
+ }
225
+
201
226
  function _rapierPagePreviewMeta(root, fallbackTitle) {
202
227
  const words = node => String(node?.textContent || '').replace(/\s+/g, ' ').trim();
203
228
  const title = words(root.querySelector('h1,h2,h3,h4,h5,h6')) || String(fallbackTitle || '');
@@ -72,7 +72,7 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
72
72
  css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
73
73
  if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
74
74
  }
75
- css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
75
+ css += scope + ' :is(li,td,th,blockquote)>:last-child:not(input){margin-bottom:0}' +
76
76
  scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
77
77
  scope + ' .table-scroll-wrap>table{margin-block:0}' +
78
78
  scope + ' pre{font-size:calc(var(--md-text-body) * .7386)}' +
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function isTemplatePage(source) {
3
+ const match = /^(?:)?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(String(source).slice(0, 65536));
4
+ return !!match && /^template:[ \t]*page[ \t]*(?:#.*)?$/m.test(match[1]);
5
+ }
6
+ export const isPageBorderLayout = layout => layout?.wrap === 'behind' && layout.lock === 'on';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
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",