rapier-markdown-kit 1.2.19 → 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.
@@ -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;}};
@@ -184,7 +184,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
184
184
  [side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
185
185
  const bodyWidth = Math.floor((width - margins.left - margins.right) * 4 / 3);
186
186
  const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
187
- 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;
188
189
  const frame = document.createElement('iframe');
189
190
  let vectors, backdrop = null, nativeGlyphs, filmSession;
190
191
  let rejectAbort;
@@ -319,7 +320,21 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
319
320
  if (imageBytes > maxBytes) refuse('export_too_large');
320
321
  pages.push({rgb: packed});
321
322
  };
322
- 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;) {
323
338
  check();
324
339
  const span = Math.min(share, count - first);
325
340
  try {
@@ -698,9 +713,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
698
713
  const marked = [];
699
714
  parentTree.push(marked);
700
715
  const held = pages[index], flate = !!held?.rgb;
701
- 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 = [];
702
719
  pages[index] = null;
703
- const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
704
720
  const emit = (drawing, left, top, clipWidth, clipHeight) => {
705
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`;
706
722
  out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
@@ -728,7 +744,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
728
744
  };
729
745
  let commands = '';
730
746
  for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
731
- 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';
732
749
  for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
733
750
  const textAlphas = new Map();
734
751
  for (const run of textPages[index]) {
@@ -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,
@@ -334,7 +343,7 @@ export function readDocumentSettings(markdown) {
334
343
  const monofont = documentFont(take('monofont'));
335
344
  const defaultfont = documentFont(take('defaultfont'));
336
345
  const linestretch = leading(take('linestretch'));
337
- const papersize = PAPERS[String(take('papersize') || '').toLowerCase()] || null;
346
+ const papersize = paperOf(take('papersize'));
338
347
  const geometry = take('geometry') != null ? marginOf(take('geometry')) : null;
339
348
  const page = String(take('pagestyle') || '').toLowerCase();
340
349
  return Object.freeze({
@@ -359,14 +368,14 @@ export function updateDocumentSettings(markdown, patch) {
359
368
  text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
360
369
  for (const [key, supplied] of Object.entries(patch || {})) {
361
370
  const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
362
- if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry'].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);
363
372
  const matches = fields.filter(item => item.key === key);
364
373
  if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
365
374
  const field = matches[0];
366
- const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(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);
367
376
  if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
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;
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;
370
379
  if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
371
380
  if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
372
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.19",
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",