rapier-markdown-kit 1.1.59 → 1.1.62
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 +6 -4
- 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() {
|
|
@@ -697,7 +699,7 @@ async function _rapierRequireOfflinePageImages(root) {
|
|
|
697
699
|
}
|
|
698
700
|
|
|
699
701
|
async function _rapierBuildSharedPage(captured) {
|
|
700
|
-
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
|
|
702
|
+
const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured?.proposalText == null ? captured : {...captured, canonical: captured.proposalText});
|
|
701
703
|
// One owner for every HTML file Rapier writes, including offline-image admission and CSP.
|
|
702
704
|
// Share adds only the nearest-side no-script float and the long-code treatment.
|
|
703
705
|
const artifact = await _rapierBuildArtifact({
|
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.62",
|
|
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",
|