rapier-markdown-kit 1.1.59 → 1.1.63
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/dist/kit/ledger/carried.mjs +51 -0
- package/dist/kit/render.mjs +16 -8
- package/dist/layout/model.mjs +46 -2
- package/dist/spec/md-layout.mjs +13 -2
- package/dist/spec/md-marks.mjs +18 -12
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {readLedger} from './format.mjs';
|
|
3
|
+
import {sha256} from './hash.mjs';
|
|
3
4
|
import {authorship as projectAuthorship, readAuthorship} from './authorship.mjs';
|
|
4
5
|
import {canonicalJSON, fault} from './data.mjs';
|
|
5
6
|
export function encodeCarried(text) {
|
|
@@ -67,3 +68,53 @@ export function readDocument(source) {
|
|
|
67
68
|
const text = source.slice(0, at);
|
|
68
69
|
return {text, ...validateParts(text, parts)};
|
|
69
70
|
}
|
|
71
|
+
|
|
72
|
+
// One portable baseline. A digest is integrity evidence, not authentication of the named proposer.
|
|
73
|
+
export function readBase(value) {
|
|
74
|
+
if (!value || typeof value !== 'object' || typeof value.text !== 'string' ||
|
|
75
|
+
typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256) || sha256(value.text) !== value.sha256)
|
|
76
|
+
throw fault('proposal base SHA-256 differs from its text');
|
|
77
|
+
if (typeof value.revision !== 'string' || !value.revision || value.revision.length > 256 || /[\u0000-\u001f\u007f]/.test(value.revision))
|
|
78
|
+
throw fault('invalid proposal base revision');
|
|
79
|
+
if (typeof value.name !== 'string' || !value.name || value.name.length > 255 || /[\\/\u0000-\u001f\u007f]/.test(value.name))
|
|
80
|
+
throw fault('invalid proposal base name');
|
|
81
|
+
if (typeof value.by !== 'string' || !value.by.trim() || value.by.length > 96 || /[\u0000-\u001f\u007f]/.test(value.by))
|
|
82
|
+
throw fault('a proposal needs its proposer name');
|
|
83
|
+
if (typeof value.at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value.at) || !Number.isFinite(Date.parse(value.at)))
|
|
84
|
+
throw fault('the time of a proposal is an ISO 8601 UTC timestamp');
|
|
85
|
+
return {text: value.text, sha256: value.sha256, revision: value.revision, name: value.name, by: value.by, at: value.at};
|
|
86
|
+
}
|
|
87
|
+
const escapeAttribute = value => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
88
|
+
const readAttribute = value => value.replace(/&(quot|lt|gt|amp);/g, (_, name) => ({quot: '"', lt: '<', gt: '>', amp: '&'})[name]);
|
|
89
|
+
export function writeBase(text, value) {
|
|
90
|
+
if (value == null) return '';
|
|
91
|
+
const base = readBase(value);
|
|
92
|
+
if (base.text === text) return '';
|
|
93
|
+
return '<script type="text/markdown" id="rapier-base"' + ['name', 'sha256', 'revision', 'by', 'at']
|
|
94
|
+
.map(key => ' data-' + key + '="' + escapeAttribute(base[key]) + '"').join('') + '>' + encodeCarried(base.text) + '</script>';
|
|
95
|
+
}
|
|
96
|
+
export function readBaseElements(elements) {
|
|
97
|
+
const rows = [...elements].filter(element => element.id === 'rapier-base');
|
|
98
|
+
if (!rows.length) return null;
|
|
99
|
+
const row = rows[0];
|
|
100
|
+
if (rows.length !== 1 || row.type !== 'text/markdown' || (row.tagName && row.tagName.toLowerCase() !== 'script'))
|
|
101
|
+
throw fault('duplicate or executable proposal base');
|
|
102
|
+
const get = name => row.getAttribute ? row.getAttribute('data-' + name) : row[name];
|
|
103
|
+
return readBase({text: decodeCarried(row.textContent), ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, get(key)]))});
|
|
104
|
+
}
|
|
105
|
+
export function takeBase(html) {
|
|
106
|
+
const elements = [];
|
|
107
|
+
const stripped = html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (whole, attrs, content) => {
|
|
108
|
+
const pairs = [...attrs.matchAll(/(?:^|\s)([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g)].map(m => [m[1].toLowerCase(), readAttribute(m[2] ?? m[3] ?? m[4])]);
|
|
109
|
+
if (!pairs.some(([key, value]) => key === 'id' && value === 'rapier-base')) return whole;
|
|
110
|
+
const seen = new Set(), values = {};
|
|
111
|
+
for (const [key, value] of pairs) {
|
|
112
|
+
if (seen.has(key)) throw fault('ambiguous proposal base');
|
|
113
|
+
seen.add(key); values[key] = value;
|
|
114
|
+
}
|
|
115
|
+
elements.push({id: values.id, type: values.type, textContent: content,
|
|
116
|
+
...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, values['data-' + key]]))});
|
|
117
|
+
return '';
|
|
118
|
+
});
|
|
119
|
+
return {html: stripped, base: readBaseElements(elements)};
|
|
120
|
+
}
|
package/dist/kit/render.mjs
CHANGED
|
@@ -406,7 +406,8 @@ async function _rapierSharedSourceCarrier(context, root) {
|
|
|
406
406
|
(forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
|
|
407
407
|
forms.carried + '\n</script>\n' +
|
|
408
408
|
// The optional edit ledger (W/4): carried only when the host binds its owner; a host without one writes none.
|
|
409
|
-
(RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried))
|
|
409
|
+
(RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
|
|
410
|
+
RapierLedgerCarried.writeBase(forms.resolved, context.proposalBase) : '');
|
|
410
411
|
}
|
|
411
412
|
|
|
412
413
|
function _rapierSharedResolve(text, ids, images, definitions) {
|
|
@@ -438,7 +439,7 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
438
439
|
const template = document.createElement('template');
|
|
439
440
|
template.innerHTML = html;
|
|
440
441
|
const invalid = () => { throw new Error('The editable source in this web page is incomplete or changed'); };
|
|
441
|
-
const carriers = template.content.querySelectorAll('script[type="' + _RAPIER_SHARED_SOURCE_TYPE + '"]');
|
|
442
|
+
const carriers = Array.from(template.content.querySelectorAll('script[type="' + _RAPIER_SHARED_SOURCE_TYPE + '"]')).filter(row => row.id !== 'rapier-base');
|
|
442
443
|
if (carriers.length !== 1) return invalid();
|
|
443
444
|
const carrier = carriers[0];
|
|
444
445
|
const name = carrier.getAttribute('data-filename') || '', kind = carrier.getAttribute('data-kind'), sha256 = carrier.getAttribute('data-sha256') || '';
|
|
@@ -463,7 +464,8 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
463
464
|
const bom = source.charCodeAt(0) === 0xFEFF;
|
|
464
465
|
if (RapierTextCodec.normalizeDocument(source) !== (bom ? source.slice(1) : source)) return invalid();
|
|
465
466
|
const parts = RapierLedgerCarried ? RapierLedgerCarried.readParts(source, Array.from(template.content.querySelectorAll('#rapier-ledger, #rapier-authorship'))) : {};
|
|
466
|
-
|
|
467
|
+
const proposalBase = RapierLedgerCarried?.readBaseElements(Array.from(template.content.querySelectorAll('#rapier-base'))) || null;
|
|
468
|
+
return {source, filename: name, kind, bom, ...parts, proposalBase};
|
|
467
469
|
}
|
|
468
470
|
|
|
469
471
|
function _rapierSharedPageFallbackCss() {
|
|
@@ -577,6 +579,10 @@ function _rapierSharedPageLayout(root) {
|
|
|
577
579
|
image.style.width = naturalWidth + 'px';
|
|
578
580
|
image.style.marginLeft = image.style.marginRight = ((bounds.width - naturalWidth) / 2) + 'px';
|
|
579
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);
|
|
580
586
|
} else {
|
|
581
587
|
if (layout.width != null) paragraph.style.width = layout.width + '%';
|
|
582
588
|
else if (image.getAttribute('width')) paragraph.style.width = image.getAttribute('width') + 'px';
|
|
@@ -697,7 +703,7 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
697
703
|
}
|
|
698
704
|
|
|
699
705
|
async function _rapierBuildSharedPage(captured) {
|
|
700
|
-
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
|
|
706
|
+
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured?.proposalText == null ? captured : {...captured, canonical: captured.proposalText});
|
|
701
707
|
// One owner for every HTML file Rapier writes, including offline-image admission and CSP.
|
|
702
708
|
// Share adds only the nearest-side no-script float and the long-code treatment.
|
|
703
709
|
const artifact = await _rapierBuildArtifact({
|
|
@@ -1306,7 +1312,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1306
1312
|
|
|
1307
1313
|
let ownerLed = false;
|
|
1308
1314
|
for (const {element: source, image, layout, width: imageWidth} of anchors.get(element) || []) {
|
|
1309
|
-
|
|
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);
|
|
1310
1318
|
if (!unrotated) { restore(); return; }
|
|
1311
1319
|
// rasterReserved (layout/model.mjs).
|
|
1312
1320
|
const rasterRad = (layout.rotate || 0) * Math.PI / 180;
|
|
@@ -1317,7 +1325,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1317
1325
|
const em = parseFloat(computed.fontSize) || 16;
|
|
1318
1326
|
const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
|
|
1319
1327
|
const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
|
|
1320
|
-
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;
|
|
1321
1329
|
if (!outOfFlow(layout)) {
|
|
1322
1330
|
for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
|
|
1323
1331
|
if (obstacle.x < left + rectangle.width + 10 && obstacle.x + obstacle.width > left - 10 &&
|
|
@@ -1325,14 +1333,14 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1325
1333
|
y = obstacle.y + obstacle.height + 10;
|
|
1326
1334
|
}
|
|
1327
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.
|
|
1328
|
-
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;
|
|
1329
1337
|
// Only the first picture of an owner takes it down.
|
|
1330
1338
|
if (!ownerLed && y > ownerTop + 0.5) {
|
|
1331
1339
|
clearTo(element, (bounds.top - origin) + (y - ownerTop), origin, source);
|
|
1332
1340
|
bounds = box(element);
|
|
1333
1341
|
}
|
|
1334
1342
|
ownerLed = true;
|
|
1335
|
-
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));
|
|
1336
1344
|
}
|
|
1337
1345
|
placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
|
|
1338
1346
|
visualLeft: left + visualDeltaX, visualTop: y + visualDeltaY, visualWidth: fit.fit.width, visualHeight: fit.fit.height,
|
package/dist/layout/model.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
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
|
+
export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent']);
|
|
7
9
|
export const alignments = new Set(['left', 'center', 'right', 'justify']);
|
|
8
10
|
// `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
|
|
9
11
|
export const wraps = new Set(['around', 'box', 'behind', 'front']);
|
|
10
12
|
const has = (value, key) => Object.hasOwn(value, key);
|
|
11
13
|
const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
|
|
14
|
+
const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
|
|
12
15
|
const level = value => Number.isInteger(value) && value >= 1 && value <= 4;
|
|
13
16
|
const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
|
|
14
17
|
value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
|
|
@@ -19,6 +22,7 @@ export function validLayout(value) {
|
|
|
19
22
|
Object.keys(value).some(key => !fields.has(key))) return false;
|
|
20
23
|
return (!has(value, 'align') || alignments.has(value.align)) &&
|
|
21
24
|
(!has(value, 'width') || percent(value.width) && value.width > 0) &&
|
|
25
|
+
(!has(value, 'lines') || lineCount(value.lines) && !has(value, 'width')) &&
|
|
22
26
|
(!has(value, 'wrap') || wraps.has(value.wrap)) &&
|
|
23
27
|
(!has(value, 'x') || percent(value.x) && (has(value, 'wrap') || has(value, 'width'))) &&
|
|
24
28
|
(!has(value, 'y') || typeof value.y === 'number' && Number.isFinite(value.y) && value.y >= -50 && has(value, 'wrap')) &&
|
|
@@ -59,6 +63,9 @@ export function parseLayout(comment) {
|
|
|
59
63
|
} else if (key === 'opacity') {
|
|
60
64
|
if (!/^[1-9]\d{0,2}%$/.test(raw)) return null;
|
|
61
65
|
value.opacity = Number(raw.slice(0, -1));
|
|
66
|
+
} else if (key === 'lines') {
|
|
67
|
+
if (!/^[1-9]\d?$/.test(raw)) return null;
|
|
68
|
+
value.lines = Number(raw);
|
|
62
69
|
} else if (key === 'first' || key === 'indent') {
|
|
63
70
|
if (!/^[1-4]$/.test(raw)) return null;
|
|
64
71
|
value[key] = Number(raw);
|
|
@@ -75,7 +82,8 @@ export function decodeLayoutAttribute(value) {
|
|
|
75
82
|
|
|
76
83
|
export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(value));
|
|
77
84
|
|
|
78
|
-
// The picture's own CSS: its normal-flow width and x, and its fade.
|
|
85
|
+
// 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
|
|
86
|
+
// picture's own text; a renderer that sets the picture beside its anchor measures that paragraph's line and cap instead.
|
|
79
87
|
export function imageStyle(value) {
|
|
80
88
|
if (!validLayout(value)) return '';
|
|
81
89
|
const parts = [];
|
|
@@ -85,10 +93,13 @@ export function imageStyle(value) {
|
|
|
85
93
|
decimal(Number(Math.max(0, Math.min(100 - width, value.x - width / 2)).toFixed(6))) + '%;margin-right:0';
|
|
86
94
|
parts.push('width:' + decimal(width) + '%;height:auto' + left);
|
|
87
95
|
}
|
|
96
|
+
if (value.lines != null) parts.push('width:auto;max-width:100%;height:' + linesHeightCss(value.lines));
|
|
88
97
|
if (value.opacity != null && value.opacity < 100) parts.push('opacity:' + decimal(value.opacity / 100));
|
|
89
98
|
return parts.join(';');
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
export const linesHeightCss = lines => lines === 1 ? '1cap' : 'calc(' + (lines - 1) + ' * var(--md-line, 1.6em) + 1cap)';
|
|
102
|
+
|
|
92
103
|
// Controls remain real DOM nodes while their surrounding text flows. A checkbox, link or
|
|
93
104
|
// disclosure is not a reason to exclude its entire block from picture placement.
|
|
94
105
|
export function wrapTextBlock(element) {
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -11,11 +11,12 @@ const inkReader = () => {
|
|
|
11
11
|
|
|
12
12
|
// Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
|
|
13
13
|
// marks, a paired comment with the colour span's shape. The opener holds the stroke:
|
|
14
|
-
// <!--ink KIND [NUMBER] [COLOUR] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
|
|
14
|
+
// <!--ink KIND [NUMBER] [COLOUR] [w=N] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
|
|
15
15
|
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
|
|
16
16
|
// absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
|
|
17
|
-
// hundredths of an em;
|
|
18
|
-
//
|
|
17
|
+
// hundredths of an em; w is the pen's width as Draw counts a nib (2 to 24; 9 is the default and is never written): the stroke is drawn 0.11 em
|
|
18
|
+
// wide at 9 and in proportion to w otherwise; PATH is the stroke, its first point absolute in the frame and the rest as
|
|
19
|
+
// moves, hundredths of an em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
|
|
19
20
|
// is ordinary comment text and marks nothing. Ink pairs do not nest in each other. Arrow/end alone take a pairing
|
|
20
21
|
// number, an end takes no other data; under, strike and arrow may omit a path for a line derived from their words.
|
|
21
22
|
const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
|
|
@@ -23,7 +24,7 @@ const inkReader = () => {
|
|
|
23
24
|
const INK_PATH_MAX = 160;
|
|
24
25
|
const INK_INT_MAX = 999999;
|
|
25
26
|
const INK_INT = '-?\\d{1,6}';
|
|
26
|
-
const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: ([1-9]\\d{0,5}))?(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
|
|
27
|
+
const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: ([1-9]\\d{0,5}))?(?: (' + COLOR_VALUE + '))?(?: w=([1-9]\\d?))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
|
|
27
28
|
+ '(?: (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*))?';
|
|
28
29
|
const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
|
|
29
30
|
|
|
@@ -32,9 +33,11 @@ const inkReader = () => {
|
|
|
32
33
|
// INK_OPEN_PATTERN and this function; isInkClose reads INK_CLOSE.
|
|
33
34
|
function readInkMatch(match) {
|
|
34
35
|
const linked = match[1] === 'arrow' || match[1] === 'end';
|
|
35
|
-
if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[
|
|
36
|
-
if (match[1] === 'end' && match.slice(3,
|
|
37
|
-
const
|
|
36
|
+
if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[9] == null)) return null;
|
|
37
|
+
if (match[1] === 'end' && match.slice(3, 10).some(value => value != null)) return null;
|
|
38
|
+
const width = match[4] == null ? null : Number(match[4]);
|
|
39
|
+
if (width != null && (width < 2 || width > 24 || width === 9)) return null;
|
|
40
|
+
const moves = match[9] == null ? [] : match[9].split(' ').map(pair => pair.split(',').map(Number));
|
|
38
41
|
if (moves.length > INK_PATH_MAX) return null;
|
|
39
42
|
const path = moves.length ? [moves[0]] : [];
|
|
40
43
|
for (let i = 1; i < moves.length; i++) {
|
|
@@ -47,8 +50,9 @@ const inkReader = () => {
|
|
|
47
50
|
kind: match[1],
|
|
48
51
|
...(linked ? { id: Number(match[2]) } : {}),
|
|
49
52
|
hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
...(width == null ? {} : { width }),
|
|
54
|
+
box: match[5] == null ? null : [Number(match[5]), Number(match[6])],
|
|
55
|
+
at: match[7] == null ? null : [Number(match[7]), Number(match[8])],
|
|
52
56
|
path,
|
|
53
57
|
});
|
|
54
58
|
}
|
|
@@ -144,7 +148,7 @@ const inkPair = (value, name, nonNegative = false) => {
|
|
|
144
148
|
return pair;
|
|
145
149
|
};
|
|
146
150
|
|
|
147
|
-
// {kind, id?, hex?, box?, at?, path?} to the opener; the path's points are absolute in the frame and become moves after the
|
|
151
|
+
// {kind, id?, hex?, width?, box?, at?, path?} to the opener; the path's points are absolute in the frame and become moves after the
|
|
148
152
|
// first. Refuses with a TypeError rather than writing a spelling the reader would not read.
|
|
149
153
|
export function formatInkOpen(mark) {
|
|
150
154
|
if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
|
|
@@ -153,7 +157,7 @@ export function formatInkOpen(mark) {
|
|
|
153
157
|
if (linked ? !Number.isInteger(mark.id) || mark.id <= 0 || mark.id > INK_INT_MAX : mark.id != null) throw new TypeError('ink: only an arrow or its end has a positive pairing number');
|
|
154
158
|
const id = linked ? ' ' + mark.id : '';
|
|
155
159
|
if (mark.kind === 'end') {
|
|
156
|
-
if (mark.hex != null || mark.box != null || mark.at != null || (mark.path != null && (!Array.isArray(mark.path) || mark.path.length))) throw new TypeError('ink: an arrow end holds only its pairing number');
|
|
160
|
+
if (mark.hex != null || mark.width != null || mark.box != null || mark.at != null || (mark.path != null && (!Array.isArray(mark.path) || mark.path.length))) throw new TypeError('ink: an arrow end holds only its pairing number');
|
|
157
161
|
return '<!--ink end' + id + '-->';
|
|
158
162
|
}
|
|
159
163
|
let colour = '';
|
|
@@ -162,6 +166,8 @@ export function formatInkOpen(mark) {
|
|
|
162
166
|
if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the colour is the colour span\'s value');
|
|
163
167
|
colour = ' ' + value;
|
|
164
168
|
}
|
|
169
|
+
if (mark.width != null && (!Number.isInteger(mark.width) || mark.width < 2 || mark.width > 24)) throw new TypeError('ink: the width is an integer from 2 to 24, and 9 is spelled by leaving it out');
|
|
170
|
+
const width = mark.width == null || mark.width === 9 ? '' : ' w=' + mark.width;
|
|
165
171
|
const box = mark.box == null ? '' : ' box=' + inkPair(mark.box, 'box', true).join(',');
|
|
166
172
|
const at = mark.at == null ? '' : ' at=' + inkPair(mark.at, 'at').join(',');
|
|
167
173
|
const optionalPath = linked || mark.kind === 'under' || mark.kind === 'strike';
|
|
@@ -170,7 +176,7 @@ export function formatInkOpen(mark) {
|
|
|
170
176
|
const points = path.map(point => inkPair(point, 'path'));
|
|
171
177
|
const moves = points.length ? [points[0].join(',')] : [];
|
|
172
178
|
for (let i = 1; i < points.length; i++) moves.push([inkInt(points[i][0] - points[i - 1][0], 'path move'), inkInt(points[i][1] - points[i - 1][1], 'path move')].join(','));
|
|
173
|
-
return '<!--ink ' + mark.kind + id + colour + box + at + (moves.length ? ' ' + moves.join(' ') : '') + '-->';
|
|
179
|
+
return '<!--ink ' + mark.kind + id + colour + width + box + at + (moves.length ? ' ' + moves.join(' ') : '') + '-->';
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
export function formatInkRun(mark, content) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.63",
|
|
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",
|