rapier-markdown-kit 1.1.62 → 1.1.64

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.
@@ -579,6 +579,10 @@ function _rapierSharedPageLayout(root) {
579
579
  image.style.width = naturalWidth + 'px';
580
580
  image.style.marginLeft = image.style.marginRight = ((bounds.width - naturalWidth) / 2) + 'px';
581
581
  }
582
+ } else if (layout.lines != null) {
583
+ // Sized in lines: the float shrinks to the picture, whose height is the lines (spec/md-layout.mjs imageStyle).
584
+ image.style.width = 'auto';
585
+ image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines);
582
586
  } else {
583
587
  if (layout.width != null) paragraph.style.width = layout.width + '%';
584
588
  else if (image.getAttribute('width')) paragraph.style.width = image.getAttribute('width') + 'px';
@@ -1308,7 +1312,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1308
1312
 
1309
1313
  let ownerLed = false;
1310
1314
  for (const {element: source, image, layout, width: imageWidth} of anchors.get(element) || []) {
1311
- const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth);
1315
+ // A picture sized in lines (`lines`) reads its anchor's line and stands on the first line's cap height, as the live editor.
1316
+ const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
1317
+ const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth, lines);
1312
1318
  if (!unrotated) { restore(); return; }
1313
1319
  // rasterReserved (layout/model.mjs).
1314
1320
  const rasterRad = (layout.rotate || 0) * Math.PI / 180;
@@ -1319,7 +1325,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1319
1325
  const em = parseFloat(computed.fontSize) || 16;
1320
1326
  const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
1321
1327
  const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
1322
- let y = bounds.top - origin + topInset + Math.min(layout.y || 0, height / em) * em;
1328
+ let y = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
1323
1329
  if (!outOfFlow(layout)) {
1324
1330
  for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
1325
1331
  if (obstacle.x < left + rectangle.width + 10 && obstacle.x + obstacle.width > left - 10 &&
@@ -1327,14 +1333,14 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1327
1333
  y = obstacle.y + obstacle.height + 10;
1328
1334
  }
1329
1335
  // R87k: a picture pushed down by an earlier one takes its owner with it. A deliberate `y` stays; only the collision push moves the paragraph.
1330
- const ownerTop = bounds.top - origin + topInset + Math.min(layout.y || 0, height / em) * em;
1336
+ const ownerTop = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
1331
1337
  // Only the first picture of an owner takes it down.
1332
1338
  if (!ownerLed && y > ownerTop + 0.5) {
1333
1339
  clearTo(element, (bounds.top - origin) + (y - ownerTop), origin, source);
1334
1340
  bounds = box(element);
1335
1341
  }
1336
1342
  ownerLed = true;
1337
- obstacles.push(...geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height));
1343
+ obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height), layout, lines, y, rectangle.height));
1338
1344
  }
1339
1345
  placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
1340
1346
  visualLeft: left + visualDeltaX, visualTop: y + visualDeltaY, visualWidth: fit.fit.width, visualHeight: fit.fit.height,
@@ -82,10 +82,54 @@ export function mapFragment(run, fragment) {
82
82
  return {text: fragment.text, offsets, start: run.offsets[from], end: run.offsets[to]};
83
83
  }
84
84
 
85
- export function imageBox(columnWidth, naturalWidth, naturalHeight, layout = {}, defaultWidth = naturalWidth) {
85
+ // The line a picture is measured in (`lines`, spec/md-layout.mjs): the anchor's line box, its type's cap height, and where that
86
+ // cap height stands in the first line box (the half-leading model: the baseline sits half the leading below the box's top plus
87
+ // the ascent). Read from the font as the page lays it out, so a text size or a line spacing moves the picture with the words.
88
+ const fontMetrics = new Map(), measurers = new WeakMap();
89
+ export function lineMetrics(element) {
90
+ const view = element?.ownerDocument?.defaultView;
91
+ if (!view) return null;
92
+ const computed = view.getComputedStyle(element), size = parseFloat(computed.fontSize) || 16;
93
+ const font = computed.fontStyle + ' ' + computed.fontWeight + ' ' + size + 'px ' + computed.fontFamily;
94
+ let metrics = fontMetrics.get(font);
95
+ if (!metrics) {
96
+ let context = measurers.get(element.ownerDocument);
97
+ if (!context) measurers.set(element.ownerDocument, context = element.ownerDocument.createElement('canvas').getContext('2d'));
98
+ if (!context) return null;
99
+ context.font = font;
100
+ const capital = context.measureText('H');
101
+ metrics = {ascent: capital.fontBoundingBoxAscent, descent: capital.fontBoundingBoxDescent, cap: capital.actualBoundingBoxAscent};
102
+ if (![metrics.ascent, metrics.descent, metrics.cap].every(finite) || !(metrics.cap > 0)) return null;
103
+ // A face still loading measures as its fallback: keep the answer only once the face is in.
104
+ if (element.ownerDocument.fonts?.check?.(font) !== false && element.ownerDocument.fonts?.status !== 'loading') fontMetrics.set(font, metrics);
105
+ }
106
+ const line = parseFloat(computed.lineHeight) || metrics.ascent + metrics.descent;
107
+ return {line, cap: metrics.cap, capTop: (line - metrics.ascent - metrics.descent) / 2 + metrics.ascent - metrics.cap};
108
+ }
109
+
110
+ // `lines` tall: from the first line's cap height to the Nth line's baseline, N - 1 lines and one cap.
111
+ export function linesHeight(lines, metrics) {
112
+ const height = Number.isInteger(lines) && lines > 0 && metrics && finite(metrics.line) && finite(metrics.cap)
113
+ ? (lines - 1) * metrics.line + metrics.cap : NaN;
114
+ return height > 0 ? height : null;
115
+ }
116
+
117
+ // A picture sized in lines keeps its standoff off the line after its last: below its foot (the Nth baseline) the gap stops
118
+ // at that line's bottom, so exactly N lines stand beside it.
119
+ export function linesSlices(slices, layout, metrics, y, height) {
120
+ if (layout?.lines == null || !metrics || !(slices?.length)) return slices;
121
+ const floor = y + height + Math.max(0, metrics.line - metrics.capTop - metrics.cap) - .5;
122
+ return slices.filter(slice => slice.y < floor).map(slice => slice.y + slice.height > floor ? {...slice, height: floor - slice.y} : slice);
123
+ }
124
+
125
+ // Where a picture's top stands in its anchor before `y`: a picture sized in lines stands on the first line's cap height.
126
+ export const linesTop = (layout, metrics) => layout?.lines != null && finite(metrics?.capTop) ? metrics.capTop : 0;
127
+
128
+ export function imageBox(columnWidth, naturalWidth, naturalHeight, layout = {}, defaultWidth = naturalWidth, metrics = null) {
86
129
  if (![columnWidth, naturalWidth, naturalHeight, defaultWidth].every(value => finite(value) && value > 0) ||
87
130
  !validLayout(layout) || layout.align === 'justify') return null;
88
- const requestedWidth = layout.width == null ? defaultWidth : columnWidth * layout.width / 100;
131
+ const tall = layout.lines == null ? null : linesHeight(layout.lines, metrics);
132
+ const requestedWidth = tall ? tall * naturalWidth / naturalHeight : layout.width == null ? defaultWidth : columnWidth * layout.width / 100;
89
133
  if (!finite(requestedWidth) || requestedWidth <= 0) return null;
90
134
  const width = Math.min(columnWidth, requestedWidth), height = width * naturalHeight / naturalWidth;
91
135
  const center = layout.x == null ? width / 2 : columnWidth * layout.x / 100;
@@ -2,13 +2,18 @@
2
2
 
3
3
  // `rotate`: degrees in (-180, 180], one decimal, omitted when 0; never written for a Rapier drawing (its turn lives in its SVG).
4
4
  // `opacity`: a picture's fade, a whole percent from 5 to 100, omitted at 100; the picture's bytes never change.
5
+ // `lines`: a picture's height in lines of the text beside it, set as a drop cap is (CSS `initial-letter`): its top on the first
6
+ // line's cap height, its foot on the Nth line's baseline, its width from its own aspect. A whole number 1 to 12; never with `width`.
5
7
  // `first`/`indent`: a paragraph's first-line indent and its whole-block indent, in levels of one step (2em), a whole number from 1 to 4, omitted when 0; text only.
6
- export const fields = new Set(['align', 'width', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent']);
8
+ // `lock=on`: a picture locked in place (the founder, 5 October): a tap goes to the words, never the picture; a hold takes it
9
+ // to unlock it. Omitted when off.
10
+ export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'lock']);
7
11
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
8
12
  // `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
9
13
  export const wraps = new Set(['around', 'box', 'behind', 'front']);
10
14
  const has = (value, key) => Object.hasOwn(value, key);
11
15
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
16
+ const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
12
17
  const level = value => Number.isInteger(value) && value >= 1 && value <= 4;
13
18
  const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
14
19
  value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
@@ -19,12 +24,14 @@ export function validLayout(value) {
19
24
  Object.keys(value).some(key => !fields.has(key))) return false;
20
25
  return (!has(value, 'align') || alignments.has(value.align)) &&
21
26
  (!has(value, 'width') || percent(value.width) && value.width > 0) &&
27
+ (!has(value, 'lines') || lineCount(value.lines) && !has(value, 'width')) &&
22
28
  (!has(value, 'wrap') || wraps.has(value.wrap)) &&
23
29
  (!has(value, 'x') || percent(value.x) && (has(value, 'wrap') || has(value, 'width'))) &&
24
30
  (!has(value, 'y') || typeof value.y === 'number' && Number.isFinite(value.y) && value.y >= -50 && has(value, 'wrap')) &&
25
31
  (!has(value, 'rotate') || oneDecimalDegrees(value.rotate)) &&
26
32
  (!has(value, 'opacity') || Number.isInteger(value.opacity) && value.opacity >= 5 && value.opacity <= 100) &&
27
33
  (!has(value, 'first') || level(value.first)) && (!has(value, 'indent') || level(value.indent)) &&
34
+ (!has(value, 'lock') || value.lock === 'on') &&
28
35
  !(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
29
36
  }
30
37
 
@@ -59,6 +66,9 @@ export function parseLayout(comment) {
59
66
  } else if (key === 'opacity') {
60
67
  if (!/^[1-9]\d{0,2}%$/.test(raw)) return null;
61
68
  value.opacity = Number(raw.slice(0, -1));
69
+ } else if (key === 'lines') {
70
+ if (!/^[1-9]\d?$/.test(raw)) return null;
71
+ value.lines = Number(raw);
62
72
  } else if (key === 'first' || key === 'indent') {
63
73
  if (!/^[1-4]$/.test(raw)) return null;
64
74
  value[key] = Number(raw);
@@ -75,7 +85,8 @@ export function decodeLayoutAttribute(value) {
75
85
 
76
86
  export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(value));
77
87
 
78
- // The picture's own CSS: its normal-flow width and x, and its fade.
88
+ // The picture's own CSS: its normal-flow width and x, its height in lines, and its fade. `1cap` is the cap height of the
89
+ // picture's own text; a renderer that sets the picture beside its anchor measures that paragraph's line and cap instead.
79
90
  export function imageStyle(value) {
80
91
  if (!validLayout(value)) return '';
81
92
  const parts = [];
@@ -85,10 +96,14 @@ export function imageStyle(value) {
85
96
  decimal(Number(Math.max(0, Math.min(100 - width, value.x - width / 2)).toFixed(6))) + '%;margin-right:0';
86
97
  parts.push('width:' + decimal(width) + '%;height:auto' + left);
87
98
  }
99
+ // Wider than the column at its own aspect, it is contained in the N-line box, never stretched.
100
+ if (value.lines != null) parts.push('width:auto;max-width:100%;height:' + linesHeightCss(value.lines) + ';object-fit:contain');
88
101
  if (value.opacity != null && value.opacity < 100) parts.push('opacity:' + decimal(value.opacity / 100));
89
102
  return parts.join(';');
90
103
  }
91
104
 
105
+ export const linesHeightCss = lines => lines === 1 ? '1cap' : 'calc(' + (lines - 1) + ' * var(--md-line, 1.6em) + 1cap)';
106
+
92
107
  // Controls remain real DOM nodes while their surrounding text flows. A checkbox, link or
93
108
  // disclosure is not a reason to exclude its entire block from picture placement.
94
109
  export function wrapTextBlock(element) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.62",
3
+ "version": "1.1.64",
4
4
  "description": "Read, render, write and style Self-contained Markdown, and carry its optional edit ledger: the shared HTML writer, semantic renderer, stylesheet, layout, marks, pictures, Will/1, replay, authorship and offline merge. MIT, no npm dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -367,7 +367,7 @@
367
367
  .md-render h1,.md-render h2,.md-render h3,.md-render h4,.md-render h5,.md-render h6{
368
368
  font-family:var(--md-font-sans);letter-spacing:-0.01em;
369
369
  line-height:var(--md-line);margin:0 0 calc(var(--md-line) / 2);color:var(--md-heading-color);text-wrap:balance}
370
- .md-render h1{font-size:calc(var(--md-text-body) * 2.4);line-height:calc(var(--md-line) * 2);font-weight:700}
370
+ .md-render h1{font-size:calc(var(--md-text-body) * 2.7);line-height:calc(var(--md-line) * 2);font-weight:620}
371
371
  .md-render h2{font-size:calc(var(--md-text-body) * 1.9);line-height:calc(var(--md-line) * 1.5);font-weight:600}
372
372
  .md-render h3{font-size:calc(var(--md-text-body) * 1.5);line-height:calc(var(--md-line) * 1.5);font-weight:600}
373
373
  .md-render h4{font-size:calc(var(--md-text-body) * 1.25);font-weight:500}
@@ -577,7 +577,7 @@
577
577
  background:var(--md-color-inset);border:1px solid var(--md-color-border);padding:1px 5px}
578
578
  .md-render pre{margin:0 0 var(--md-line);padding:var(--md-space-3) var(--md-space-4);
579
579
  background:var(--md-color-surface-2);border:1px solid var(--md-color-border);overflow-x:auto}
580
- .md-render pre code{background:none;border:none;padding:0;font-size:0.8125rem;line-height:calc(var(--md-line) * .75)}
580
+ .md-render pre code{background:none;border:none;padding:0;font-size:calc(var(--md-text-body) * 0.7386);line-height:calc(var(--md-line) * .75)}
581
581
  .md-render .math-display-wrap{
582
582
  display:block;
583
583
  max-width:100%;
@@ -784,11 +784,18 @@
784
784
  .rapier-ink-layer svg{position:absolute;display:block;margin:0;padding:0;overflow:visible}
785
785
  .rapier-ink-layer path{fill:light-dark(var(--md-ink),var(--md-ink-dark));stroke:none;opacity:.92}
786
786
  @media print{.rapier-ink-layer path{fill:var(--md-ink)}}
787
- .md-render [data-md-color="#1c7547"]{--md-color-dark:#70c390}
788
787
  .md-render [data-md-color="#b32034"]{--md-color-dark:#ff8385}
789
- .md-render [data-md-color="#175dc4"]{--md-color-dark:#7bafff}
788
+ .md-render [data-md-color="#c2410c"]{--md-color-dark:#fb923c}
790
789
  .md-render [data-md-color="#865818"]{--md-color-dark:#d6a367}
790
+ .md-render [data-md-color="#4d7c0f"]{--md-color-dark:#a3cf5c}
791
+ .md-render [data-md-color="#1c7547"]{--md-color-dark:#70c390}
792
+ .md-render [data-md-color="#0f766e"]{--md-color-dark:#5eead4}
793
+ .md-render [data-md-color="#0369a1"]{--md-color-dark:#7dd3fc}
794
+ .md-render [data-md-color="#175dc4"]{--md-color-dark:#7bafff}
795
+ .md-render [data-md-color="#4338ca"]{--md-color-dark:#a5b4fc}
791
796
  .md-render [data-md-color="#7924d7"]{--md-color-dark:#ba97ff}
797
+ .md-render [data-md-color="#be185d"]{--md-color-dark:#f9a8d4}
798
+ .md-render [data-md-color="#57606a"]{--md-color-dark:#aab1bb}
792
799
  .md-render abbr[title]{text-decoration:underline dotted;text-decoration-color:var(--md-color-text-muted);text-underline-offset:2px;cursor:help}
793
800
  .md-render ins{text-decoration:underline;color:var(--md-color-success)}
794
801
  /* The document owns its selection accent, including on an exported page. */