rapier-markdown-kit 1.1.24 → 1.1.28

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.
@@ -2,12 +2,14 @@
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
- export const fields = new Set(['align', 'width', 'wrap', 'x', 'y', 'rotate', 'opacity']);
5
+ // `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']);
6
7
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
7
8
  // `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
8
9
  export const wraps = new Set(['around', 'box', 'behind', 'front']);
9
10
  const has = (value, key) => Object.hasOwn(value, key);
10
11
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
12
+ const level = value => Number.isInteger(value) && value >= 1 && value <= 4;
11
13
  const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
12
14
  value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
13
15
 
@@ -22,6 +24,7 @@ export function validLayout(value) {
22
24
  (!has(value, 'y') || typeof value.y === 'number' && Number.isFinite(value.y) && value.y >= -50 && has(value, 'wrap')) &&
23
25
  (!has(value, 'rotate') || oneDecimalDegrees(value.rotate)) &&
24
26
  (!has(value, 'opacity') || Number.isInteger(value.opacity) && value.opacity >= 5 && value.opacity <= 100) &&
27
+ (!has(value, 'first') || level(value.first)) && (!has(value, 'indent') || level(value.indent)) &&
25
28
  !(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
26
29
  }
27
30
 
@@ -56,6 +59,9 @@ export function parseLayout(comment) {
56
59
  } else if (key === 'opacity') {
57
60
  if (!/^[1-9]\d{0,2}%$/.test(raw)) return null;
58
61
  value.opacity = Number(raw.slice(0, -1));
62
+ } else if (key === 'first' || key === 'indent') {
63
+ if (!/^[1-4]$/.test(raw)) return null;
64
+ value[key] = Number(raw);
59
65
  } else value[key] = raw;
60
66
  }
61
67
  return validLayout(value) ? value : null;
@@ -55,6 +55,10 @@ export function scanColorMarkers(source) {
55
55
  return markers;
56
56
  }
57
57
 
58
+ export function pairColorMarkers(source) {
59
+ return pairMarkers(source, 'color');
60
+ }
61
+
58
62
  export function stripColorMarkers(text) {
59
63
  return String(text == null ? '' : text).replace(new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->', 'g'), '');
60
64
  }
@@ -63,6 +67,168 @@ export function hasColorMarker(text) {
63
67
  return new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->').test(String(text == null ? '' : text));
64
68
  }
65
69
 
70
+ // Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
71
+ // marks, a paired comment with the colour span's shape. The opener holds the stroke:
72
+ // <!--ink KIND [COLOUR] [box=W,H] [at=X,Y] PATH-->words<!--/ink-->
73
+ // KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
74
+ // absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
75
+ // hundredths of an em; PATH is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an
76
+ // em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
77
+ // is ordinary comment text and marks nothing. Ink pairs do not nest in each other.
78
+ const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free']);
79
+ const INK_CLOSE = '<!--/ink-->';
80
+ const INK_PATH_MAX = 160;
81
+ const INK_INT_MAX = 999999;
82
+ const INK_INT = '-?\\d{1,6}';
83
+ const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
84
+ + ' (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*)';
85
+ const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
86
+
87
+ export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX };
88
+
89
+ const inkInt = (value, name) => {
90
+ if (!Number.isInteger(value) || Math.abs(value) > INK_INT_MAX) throw new TypeError('ink: ' + name + ' must be an integer within ' + INK_INT_MAX);
91
+ return value;
92
+ };
93
+ const inkPair = (value, name, nonNegative = false) => {
94
+ if (!Array.isArray(value) || value.length !== 2) throw new TypeError('ink: ' + name + ' must be a pair of integers');
95
+ const pair = [inkInt(value[0], name), inkInt(value[1], name)];
96
+ if (nonNegative && (pair[0] < 0 || pair[1] < 0)) throw new TypeError('ink: ' + name + ' must not be negative');
97
+ return pair;
98
+ };
99
+
100
+ // {kind, hex?, box?, at?, path} to the opener; the path's points are absolute in the frame and become moves after the
101
+ // first. Refuses with a TypeError rather than writing a spelling the reader would not read.
102
+ export function formatInkOpen(mark) {
103
+ if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
104
+ if (!INK_KINDS.includes(mark.kind)) throw new TypeError('ink: the kind is one of ' + INK_KINDS.join(', '));
105
+ let colour = '';
106
+ if (mark.hex != null) {
107
+ const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
108
+ if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the colour is the colour span\'s value');
109
+ colour = ' ' + value;
110
+ }
111
+ const box = mark.box == null ? '' : ' box=' + inkPair(mark.box, 'box', true).join(',');
112
+ const at = mark.at == null ? '' : ' at=' + inkPair(mark.at, 'at').join(',');
113
+ if (!Array.isArray(mark.path) || !mark.path.length || mark.path.length > INK_PATH_MAX) throw new TypeError('ink: the path holds 1 to ' + INK_PATH_MAX + ' points');
114
+ const points = mark.path.map(point => inkPair(point, 'path'));
115
+ const moves = [points[0].join(',')];
116
+ 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(','));
117
+ return '<!--ink ' + mark.kind + colour + box + at + ' ' + moves.join(' ') + '-->';
118
+ }
119
+
120
+ export function formatInkRun(mark, content) {
121
+ return formatInkOpen(mark) + content + INK_CLOSE;
122
+ }
123
+
124
+ function readInkMatch(match) {
125
+ const moves = match[7].split(' ').map(pair => pair.split(',').map(Number));
126
+ if (moves.length > INK_PATH_MAX) return null;
127
+ const path = [moves[0]];
128
+ for (let i = 1; i < moves.length; i++) {
129
+ const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
130
+ if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
131
+ path.push(point);
132
+ }
133
+ const colour = match[2] || null;
134
+ return Object.freeze({
135
+ kind: match[1],
136
+ hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
137
+ box: match[3] == null ? null : [Number(match[3]), Number(match[4])],
138
+ at: match[5] == null ? null : [Number(match[5]), Number(match[6])],
139
+ path,
140
+ });
141
+ }
142
+
143
+ // Returns {mark, length in UTF-16 units} or null; not end-anchored.
144
+ export function matchInkOpen(text) {
145
+ if (typeof text !== 'string') return null;
146
+ const match = new RegExp('^' + INK_OPEN_PATTERN).exec(text);
147
+ if (!match) return null;
148
+ const mark = readInkMatch(match);
149
+ return mark && { mark, length: match[0].length };
150
+ }
151
+
152
+ export function parseInkOpen(comment) {
153
+ if (typeof comment !== 'string') return null;
154
+ const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
155
+ return match && readInkMatch(match);
156
+ }
157
+
158
+ // The opener's body is what a rendered span carries (a renderer's sanitizer drops an attribute whose value holds the
159
+ // comment's own close): the opener after `<!--ink ` and before `-->`, so the attribute reads `under red box=… 0,95 …`,
160
+ // and only a body that reads as a mark goes back. Read and write through the same grammar, so an invalid body cannot
161
+ // invent a mark. Relocated into the parse Worker with the three above: reads INK_OPEN_PATTERN and readInkMatch alone.
162
+ export function inkOpenBody(opener) {
163
+ if (typeof opener !== 'string') return null;
164
+ const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
165
+ return match && readInkMatch(match) ? opener.slice(8, -3) : null;
166
+ }
167
+ export function parseInkBody(body) {
168
+ return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
169
+ }
170
+ export function formatInkOpenFromBody(body) {
171
+ return parseInkBody(body) ? '<!--ink ' + body + '-->' : null;
172
+ }
173
+
174
+ export function isInkClose(comment) {
175
+ return comment === INK_CLOSE;
176
+ }
177
+
178
+ // Every ink marker in order: {start, end, mark} for an opener (mark null for a closer, and an opener whose path
179
+ // overflows is not a marker at all).
180
+ export function scanInkMarkers(source) {
181
+ const text = String(source == null ? '' : source);
182
+ const scan = new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-'), 'g');
183
+ const markers = [];
184
+ let match;
185
+ while ((match = scan.exec(text))) {
186
+ if (match[0] === INK_CLOSE) { markers.push({ start: match.index, end: match.index + match[0].length, mark: null, close: true }); continue; }
187
+ const mark = readInkMatch(match);
188
+ if (mark) markers.push({ start: match.index, end: match.index + match[0].length, mark, close: false });
189
+ }
190
+ return markers;
191
+ }
192
+
193
+ // The runs: an opener and the next closer with no opener between them. A marker that pairs with nothing is text
194
+ // (it shows nothing and marks nothing) and is listed under strays.
195
+ export function pairInkMarkers(source) {
196
+ return pairMarkers(source, 'ink');
197
+ }
198
+
199
+ // Colour keeps the first opener, as its inline parser does; ink replaces an unclosed opener.
200
+ // Neither kind nests in itself. The shared range shape lets custody owners treat both alike.
201
+ export function pairMarkers(source, kind) {
202
+ if (kind !== 'ink' && kind !== 'color') throw new TypeError('Unknown paired marker kind');
203
+ const color = kind === 'color';
204
+ const runs = [], strays = [];
205
+ let open = null;
206
+ for (const marker of color ? scanColorMarkers(source) : scanInkMarkers(source)) {
207
+ if (color ? marker.hex !== null : !marker.close) {
208
+ if (open) {
209
+ strays.push(color ? marker : open);
210
+ if (color) continue;
211
+ }
212
+ open = marker;
213
+ continue;
214
+ }
215
+ if (!open) { strays.push(marker); continue; }
216
+ runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start,
217
+ ...(color ? { hex: open.hex } : { mark: open.mark }) });
218
+ open = null;
219
+ }
220
+ if (open) strays.push(open);
221
+ return { runs, strays };
222
+ }
223
+
224
+ export function stripInkMarkers(text) {
225
+ return String(text == null ? '' : text).replace(new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-'), 'g'), '');
226
+ }
227
+
228
+ export function hasInkMarker(text) {
229
+ return new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-')).test(String(text == null ? '' : text));
230
+ }
231
+
66
232
  // The only spelling.
67
233
  export function formatPageBreak() {
68
234
  return PAGE_BREAK_MARKER;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.24",
3
+ "version": "1.1.28",
4
4
  "description": "Read, write and style Self-contained Markdown: the universal stylesheet, picture layout, text marks, picture appendix, line planner and Will/1 markers. MIT, no dependencies.",
5
5
  "author": "Jack Skipworth",
6
6
  "license": "MIT",
package/style.css CHANGED
@@ -95,6 +95,14 @@
95
95
  :where(.md-render) [data-md-align="center"]{text-align:center!important}
96
96
  :where(.md-render) [data-md-align="right"]{text-align:right!important}
97
97
  :where(.md-render) [data-md-align="justify"]{text-align:justify!important}
98
+ :where(.md-render) p[data-md-first="1"]{text-indent:2em}
99
+ :where(.md-render) p[data-md-first="2"]{text-indent:4em}
100
+ :where(.md-render) p[data-md-first="3"]{text-indent:6em}
101
+ :where(.md-render) p[data-md-first="4"]{text-indent:8em}
102
+ :where(.md-render) p[data-md-indent="1"]{margin-inline-start:2em}
103
+ :where(.md-render) p[data-md-indent="2"]{margin-inline-start:4em}
104
+ :where(.md-render) p[data-md-indent="3"]{margin-inline-start:6em}
105
+ :where(.md-render) p[data-md-indent="4"]{margin-inline-start:8em}
98
106
  .md-render p[data-md-align="center"]>img{margin-inline:auto}
99
107
  .md-render p[data-md-align="right"]>img{margin-left:auto;margin-right:0}
100
108
  :where(.md-render) p[data-md-layout-tight]{margin-block:0!important}