rapier-markdown-kit 1.1.14 → 1.1.19
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/agent/will.mjs +3 -1
- package/dist/kit/conformance/README.md +15 -7
- package/dist/kit/conformance/run.mjs +7 -5
- package/dist/layout/line-plan.mjs +101 -2
- package/dist/layout/model.mjs +46 -18
- package/dist/spec/md-assets.mjs +46 -0
- package/package.json +1 -1
- package/style.css +50 -42
package/dist/agent/will.mjs
CHANGED
|
@@ -28,11 +28,13 @@ function _rapierWillMarkerOf(content) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
const near = fault => ({ kind: 'near', fault, law: '', intent: null, content: line });
|
|
31
|
+
const afterPrefix = line.slice(_RAPIER_WILL_OPENER_PREFIX.length);
|
|
32
|
+
const versionGap = afterPrefix.indexOf(' ');
|
|
33
|
+
if ((versionGap < 0 ? afterPrefix : afterPrefix.slice(0, versionGap)) !== '1') return near('unknown_version');
|
|
31
34
|
if (!line.endsWith(_RAPIER_WILL_CLOSE_SUFFIX)) return near('malformed_marker');
|
|
32
35
|
const middle = line.slice(_RAPIER_WILL_OPENER_PREFIX.length,
|
|
33
36
|
line.length - _RAPIER_WILL_CLOSE_SUFFIX.length);
|
|
34
37
|
const gap = middle.indexOf(' ');
|
|
35
|
-
if ((gap < 0 ? middle : middle.slice(0, gap)) !== '1') return near('unknown_version');
|
|
36
38
|
if (gap < 0) return near('malformed_marker');
|
|
37
39
|
const rest = middle.slice(gap + 1);
|
|
38
40
|
const word = (/^[^:\s]*/.exec(rest))[0];
|
|
@@ -5,7 +5,10 @@ standard](../../docs/markdown-standard.md): inline (no layout comment), width/x/
|
|
|
5
5
|
with a real alpha silhouette, `wrap=box`, `behind`, `front`, `rotate=` on a plain raster, a foreign
|
|
6
6
|
SVG "drawing" already turned in its own bytes, a ring drawing whose unfilled interior takes a line of
|
|
7
7
|
text, both-sides wrapping around a centred picture, the neighbour search skipping a second, unrelated
|
|
8
|
-
picture,
|
|
8
|
+
picture, a heading after a picture, and RTL (Arabic) text.
|
|
9
|
+
|
|
10
|
+
Headings can anchor images. `12-heading-barrier.expected.json` records an earlier paragraph
|
|
11
|
+
anchor; that historical measurement is not a normative neighbour-selection expectation.
|
|
9
12
|
|
|
10
13
|
## What each document's JSON carries
|
|
11
14
|
|
|
@@ -78,12 +81,17 @@ Prints two separate tables (preservation and presentation are different claims):
|
|
|
78
81
|
either way), **9 CLOSE, 0 DIFFERS**, each CLOSE row's cause named rather than left as "close
|
|
79
82
|
enough": `01-inline`/`02-width-x-align`/`05-behind`/`06-front` have no real trace at all (see
|
|
80
83
|
`measurements` above) so still rest on the character-sum fallback; `03-wrap-around-silhouette`,
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
`11-neighbour-skips-image` and `13-rtl-text` run Pretext for real during export (`13-rtl-text` at
|
|
85
|
+
100% — every query of its own answered from the real trace) yet still carry a small, constant
|
|
86
|
+
left-edge delta (5.18px) that is a picture-obstacle *geometry* fact, not a text one: their
|
|
87
|
+
`profileHint` is a hand-computed, quantized alpha-band descriptor for a picture Rapier's own export
|
|
88
|
+
never serializes an occupancy descriptor for, and that descriptor's own precision — not the
|
|
89
|
+
measurer — is the residual gap. `09-drawing-ring-interior` plans the recorded 14 lines, but one row
|
|
90
|
+
differs by a whole slot (115.08px left, 292.09px right): its recorded trace, taken before a word
|
|
91
|
+
that must break inside itself was given the widest slot of its row, cuts "ordinary" into "ordinar"
|
|
92
|
+
and "y" in the ring's narrow left slot, where the planner now leaves that slot empty and sets the
|
|
93
|
+
word whole in the wide one (`layout/line-plan.mjs`). Its `expected.json` still holds the earlier cut;
|
|
94
|
+
it is regenerated from a browser export, not by hand. Before this
|
|
87
95
|
measurer existed (a per-character glyph-sum with no real trace at all), the same run reported 4
|
|
88
96
|
AGREE / 8 CLOSE / 1 DIFFERS — `13-rtl-text` disagreed by a whole line (7 expected, 8 planned): real
|
|
89
97
|
Arabic shaping (ligatures, diacritic combination) changes a run's total measured width in a way an
|
|
@@ -109,14 +109,16 @@ console.log(`\n${agree}/${rows.length} documents AGREE within ${args.tolerance}p
|
|
|
109
109
|
console.log(`Measurer: ${measurer.stats.hits} queries answered from Rapier's own real browser trace, ${measurer.stats.misses} fell back to a character-glyph sum (no kerning/shaping).`);
|
|
110
110
|
console.log(`A row's own hits/misses can undercount its real coverage: several fixtures share one identical anchor sentence, and Pretext's own per-process`);
|
|
111
111
|
console.log(`segment cache (agent/vendor/pretext/measurement.js) serves a repeated (font, segment) query straight from that cache without calling`);
|
|
112
|
-
console.log(`measureText again at all -- real data, just not counted against the SECOND document to ask for it.
|
|
113
|
-
console.log(`reads CLOSE rather than AGREE,
|
|
112
|
+
console.log(`measureText again at all -- real data, just not counted against the SECOND document to ask for it. Three named, separate reasons a row still`);
|
|
113
|
+
console.log(`reads CLOSE rather than AGREE, none of them "close enough": (a) no real trace exists at all for 01-inline/02-width-x-align/05-behind/06-front`);
|
|
114
114
|
console.log(`-- their picture pushes no flow obstacle, so layout/interchange.js's own prepare() never calls Pretext for this paragraph during export (see`);
|
|
115
|
-
console.log(`measurer.mjs) -- the from-scratch character-sum approximation disagrees with real shaping by a bounded, named amount; (b) 03/
|
|
115
|
+
console.log(`measurer.mjs) -- the from-scratch character-sum approximation disagrees with real shaping by a bounded, named amount; (b) 03/11/13 DO run`);
|
|
116
116
|
console.log(`Pretext for real (hits > 0, 13-rtl-text at 100% -- every query answered from the real trace) yet still carry a small, constant left-edge delta`);
|
|
117
|
-
console.log(`(5.18px
|
|
117
|
+
console.log(`(5.18px, unchanged from before this measurer existed) -- a picture-obstacle geometry fact, not a text one: these`);
|
|
118
118
|
console.log(`fixtures' profileHint is a hand-computed, quantized alpha band descriptor for a raster Rapier's own export never serialized an occupancy`);
|
|
119
|
-
console.log(`descriptor for (conformance/README.md, "profileHint"), and that descriptor's own precision -- not this lane's measurer -- is the residual gap
|
|
119
|
+
console.log(`descriptor for (conformance/README.md, "profileHint"), and that descriptor's own precision -- not this lane's measurer -- is the residual gap; (c) 09 plans the recorded 14 lines but one row`);
|
|
120
|
+
console.log(`differs by a whole slot: its recorded trace cuts a word in the ring's narrow left slot, and the planner now gives a word that must break inside itself`);
|
|
121
|
+
console.log(`the widest slot of its row (layout/line-plan.mjs) -- the recorded lines are the earlier planner's.`);
|
|
120
122
|
if (measurer.stats.missedQueries.length) {
|
|
121
123
|
console.log(`\nFirst ${Math.min(10, measurer.stats.missedQueries.length)} of ${measurer.stats.misses} missed (font, text) queries (fell back to the character sum):`);
|
|
122
124
|
for (const {font, text} of measurer.stats.missedQueries.slice(0, 10)) console.log(` ${JSON.stringify(text)} (${font})`);
|
|
@@ -1,4 +1,89 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
// A word or an address wider than its slot has to break inside itself; the browser would let it run out of the column, a
|
|
3
|
+
// projection cannot. Pretext breaks it after the last hyphen that fits, else after the last letter that fits. A person reads an
|
|
4
|
+
// address better broken where it has its own punctuation, so the break moves back to the last of these that fits.
|
|
5
|
+
const naturalBreak = /[/._?&=]/, hyphen = /[-\u058a\u2010\u2012\u2013\u2014]/;
|
|
6
|
+
let clusters;
|
|
7
|
+
const lettersOf = new WeakMap();
|
|
8
|
+
|
|
9
|
+
function breakAtPunctuation(flow, range, cursor, layoutNextRichInlineLineRange) {
|
|
10
|
+
const last = range.fragments.at(-1), end = last?.end;
|
|
11
|
+
if (!end || end.graphemeIndex === 0) return range;
|
|
12
|
+
const prepared = flow.items[last.itemIndex]?.prepared, segment = prepared?.segments?.[end.segmentIndex];
|
|
13
|
+
if (typeof segment !== 'string') return range;
|
|
14
|
+
// A long address is cut once per line; its letters are counted once.
|
|
15
|
+
let byIndex = lettersOf.get(prepared);
|
|
16
|
+
if (!byIndex) lettersOf.set(prepared, byIndex = new Map());
|
|
17
|
+
let letters = byIndex.get(end.segmentIndex);
|
|
18
|
+
if (!letters) {
|
|
19
|
+
clusters ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
|
20
|
+
byIndex.set(end.segmentIndex, letters = [...clusters.segment(segment)].map(part => part.segment));
|
|
21
|
+
}
|
|
22
|
+
const from = last.start.segmentIndex === end.segmentIndex ? last.start.graphemeIndex : 0;
|
|
23
|
+
if (end.graphemeIndex > letters.length || hyphen.test(letters[end.graphemeIndex - 1]) || naturalBreak.test(letters[end.graphemeIndex - 1])) return range;
|
|
24
|
+
let target = end.graphemeIndex - 1;
|
|
25
|
+
while (target > from && !naturalBreak.test(letters[target - 1])) target--;
|
|
26
|
+
if (target <= from) return range;
|
|
27
|
+
// Each narrower limit yields the break one letter back (`balanced` walks the breaks of a line the same way).
|
|
28
|
+
for (let limit = range.width - .05, at = end.graphemeIndex; at > target;) {
|
|
29
|
+
const shorter = layoutNextRichInlineLineRange(flow, limit, cursor), tail = shorter?.fragments.at(-1);
|
|
30
|
+
if (!tail || tail.itemIndex !== last.itemIndex || tail.end.segmentIndex !== end.segmentIndex || tail.end.graphemeIndex >= at) return range;
|
|
31
|
+
if (tail.end.graphemeIndex === target) return shorter;
|
|
32
|
+
at = tail.end.graphemeIndex;
|
|
33
|
+
limit = shorter.width - .05;
|
|
34
|
+
}
|
|
35
|
+
return range;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Pretext lets a line end wherever one element ends and the next begins. The browser allows no break there unless white space
|
|
39
|
+
// stands between them, so a comma, a colon or a closing bracket straight after `agents.md` (a chip, a link, bold words) would
|
|
40
|
+
// start the next line alone. The line ends one break earlier instead, and the punctuation keeps its word.
|
|
41
|
+
const closing = /^[,.:;!?)\]}%\u2019\u201d\u00bb\u203a\u2026\u060c\u061b\u061f\u3001\u3002\uff0c\uff0e\uff1a\uff1b\uff01\uff1f\uff09\u300d\u300f]/;
|
|
42
|
+
|
|
43
|
+
function stranding(flow, range) {
|
|
44
|
+
const {itemIndex, segmentIndex, graphemeIndex} = range.end;
|
|
45
|
+
if (segmentIndex !== 0 || graphemeIndex !== 0) return false;
|
|
46
|
+
for (let index = itemIndex; index < flow.items.length; index++) {
|
|
47
|
+
const item = flow.items[index];
|
|
48
|
+
if (!item?.establishesLine) continue;
|
|
49
|
+
return !item.breakBefore && closing.test(item.prepared?.segments?.[0] ?? '');
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function keepWithWord(flow, range, cursor, layoutNextRichInlineLineRange) {
|
|
55
|
+
if (!stranding(flow, range)) return range;
|
|
56
|
+
for (let limit = range.width - .05, tries = 64; tries > 0; tries--) {
|
|
57
|
+
const shorter = layoutNextRichInlineLineRange(flow, limit, cursor);
|
|
58
|
+
if (!shorter || shorter.width > limit + .05) return range;
|
|
59
|
+
if (!stranding(flow, shorter)) return shorter;
|
|
60
|
+
limit = shorter.width - .05;
|
|
61
|
+
}
|
|
62
|
+
return range;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A chip (inline code, a box each piece of it repeats) breaks at its own spaces and hyphens like any words, but never across a
|
|
66
|
+
// picture: "npx rapier-" left of the picture and "html" right of it read as two things. When a line would end inside a chip
|
|
67
|
+
// with another slot of the row still to come, the line ends before the chip instead and the chip goes whole to the next slot;
|
|
68
|
+
// when nothing but the chip stands in the slot, the row's remaining slots are given up and the chip's pieces stack in this one.
|
|
69
|
+
function insideChip(flow, range) {
|
|
70
|
+
const {itemIndex, segmentIndex, graphemeIndex} = range.end;
|
|
71
|
+
return (segmentIndex > 0 || graphemeIndex > 0) && flow.items[itemIndex]?.extraWidth > 0;
|
|
72
|
+
}
|
|
73
|
+
function beforeChip(flow, range, cursor, layoutNextRichInlineLineRange) {
|
|
74
|
+
let first = range.end.itemIndex;
|
|
75
|
+
while (first > 0 && flow.items[first - 1]?.extraWidth > 0) first--;
|
|
76
|
+
if (first === 0 || first <= cursor.itemIndex && (cursor.segmentIndex > 0 || cursor.graphemeIndex > 0 || first < cursor.itemIndex)) return null;
|
|
77
|
+
const before = shorter => shorter.end.itemIndex < first || shorter.end.itemIndex === first && shorter.end.segmentIndex === 0 && shorter.end.graphemeIndex === 0;
|
|
78
|
+
for (let limit = range.width - .05, tries = 64; tries > 0; tries--) {
|
|
79
|
+
const shorter = layoutNextRichInlineLineRange(flow, limit, cursor);
|
|
80
|
+
if (!shorter || shorter.width > limit + .05) return null;
|
|
81
|
+
if (before(shorter)) return shorter.end.itemIndex === cursor.itemIndex && shorter.end.segmentIndex === cursor.segmentIndex && shorter.end.graphemeIndex === cursor.graphemeIndex ? null : shorter;
|
|
82
|
+
limit = shorter.width - .05;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
2
87
|
export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange) {
|
|
3
88
|
const lines = [], ranges = [];
|
|
4
89
|
let cursor = {itemIndex: 0, segmentIndex: 0, graphemeIndex: 0}, y = 0, bottom = 0, rows = 0;
|
|
@@ -6,16 +91,30 @@ export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, dire
|
|
|
6
91
|
const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
|
|
7
92
|
if (!bands) return null;
|
|
8
93
|
if (direction === 'rtl') bands.reverse();
|
|
94
|
+
const widest = Math.max(0, ...bands.map(band => band.width));
|
|
9
95
|
let advanced = false;
|
|
10
|
-
for (
|
|
11
|
-
const
|
|
96
|
+
for (let slot = 0; slot < bands.length; slot++) {
|
|
97
|
+
const band = bands[slot];
|
|
98
|
+
let range = layoutNextRichInlineLineRange(flow, band.width, cursor);
|
|
12
99
|
if (!range) return lines.length ? {lines: balance ? balanced(flow, width, balance, lines, ranges, lineHeight, layoutNextRichInlineLineRange, materializeRichInlineLineRange) : lines, height: Math.max(lineHeight, bottom)} : null;
|
|
13
100
|
if (range.width > width + .5 || range.end.itemIndex === cursor.itemIndex &&
|
|
14
101
|
range.end.segmentIndex === cursor.segmentIndex && range.end.graphemeIndex === cursor.graphemeIndex) return null;
|
|
15
102
|
if (range.width > band.width + .5) continue;
|
|
103
|
+
range = breakAtPunctuation(flow, range, cursor, layoutNextRichInlineLineRange);
|
|
104
|
+
// Ending a line earlier for a comma can cut a word too, so that cut is settled as well.
|
|
105
|
+
if (!balance && stranding(flow, range)) range = breakAtPunctuation(flow, keepWithWord(flow, range, cursor, layoutNextRichInlineLineRange), cursor, layoutNextRichInlineLineRange);
|
|
106
|
+
// A word or address that must break inside itself does it in the widest slot of the row. A narrower one, a sliver beside the
|
|
107
|
+
// picture, waits and the widest takes it, so the head of an address, or "passage" and "s,", is never left alone in a corner.
|
|
108
|
+
if (range.end.graphemeIndex > 0 && band.width < widest - .5) continue;
|
|
109
|
+
let column = false;
|
|
110
|
+
if (!balance && slot + 1 < bands.length && insideChip(flow, range)) {
|
|
111
|
+
const whole = beforeChip(flow, range, cursor, layoutNextRichInlineLineRange);
|
|
112
|
+
if (whole) range = whole; else column = true;
|
|
113
|
+
}
|
|
16
114
|
if (lines.length === 4096) return null;
|
|
17
115
|
lines.push({...materializeRichInlineLineRange(flow, range), ...band, y}); ranges.push(range);
|
|
18
116
|
cursor = range.end; advanced = true; bottom = y + lineHeight;
|
|
117
|
+
if (column) break;
|
|
19
118
|
}
|
|
20
119
|
if (advanced) { y += lineHeight; rows++; continue; }
|
|
21
120
|
let next = Infinity;
|
package/dist/layout/model.mjs
CHANGED
|
@@ -110,36 +110,45 @@ export function rasterReserved(columnWidth, unrotated, rad) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
const shapeGrid = 48, alphaThreshold = .1, profileBands = new WeakMap();
|
|
113
|
+
const shapeGrid = 48, alphaThreshold = .1, alphaOversample = 4, profileBands = new WeakMap();
|
|
114
|
+
// Slices are cut at up to this many bands: a picture shown 6 px a band or finer keeps every row the profile holds.
|
|
115
|
+
const sliceGridMax = shapeGrid * alphaOversample, slicePixelsPerBand = 6;
|
|
114
116
|
|
|
117
|
+
// The picture's silhouette as 192 bands of horizontal runs, read from its alpha at that resolution both ways: a band holds the
|
|
118
|
+
// ink of one sampled row, and a run's ends fall on a 192nd of the picture's width, so the standoff a slice adds (`pictureSlices`'
|
|
119
|
+
// `gap`) is the least distance from the words to the paint however large the picture is drawn. At 48 rows and columns a run's
|
|
120
|
+
// end drifted by a 48th of the width (9 px at 430), a thin stroke fell between two samples, and a tall picture's bands were
|
|
121
|
+
// 20 px high. The descriptor an exported page carries (`serializeProfile`) still writes 48 bands.
|
|
115
122
|
export function alphaProfile(image) {
|
|
116
123
|
const src = image.currentSrc || image.getAttribute('src') || '';
|
|
117
124
|
if (!image.complete || !image.naturalWidth || !image.naturalHeight || !src || /^data:image\/jpe?g[;,]/i.test(src)) return null;
|
|
125
|
+
const fine = shapeGrid * alphaOversample;
|
|
118
126
|
const canvas = image.ownerDocument.createElement('canvas');
|
|
119
|
-
canvas.width = canvas.height =
|
|
127
|
+
canvas.width = canvas.height = fine;
|
|
120
128
|
let data;
|
|
121
129
|
try {
|
|
122
130
|
const context = canvas.getContext('2d', {willReadFrequently: true});
|
|
123
131
|
if (!context) return null;
|
|
124
|
-
context.
|
|
125
|
-
|
|
132
|
+
context.imageSmoothingQuality = 'high';
|
|
133
|
+
context.drawImage(image, 0, 0, fine, fine);
|
|
134
|
+
data = context.getImageData(0, 0, fine, fine).data;
|
|
126
135
|
} catch (_) { return null; }
|
|
127
136
|
finally { canvas.width = canvas.height = 0; }
|
|
128
137
|
const bands = [];
|
|
129
138
|
let opaque = true;
|
|
130
|
-
for (let y = 0; y <
|
|
139
|
+
for (let y = 0; y < fine; y++) {
|
|
131
140
|
const runs = [];
|
|
132
141
|
let start = -1;
|
|
133
|
-
for (let x = 0; x <=
|
|
134
|
-
const alpha = x <
|
|
135
|
-
if (x <
|
|
136
|
-
if (alpha > alphaThreshold * 255) { if (start < 0) start = x; }
|
|
137
|
-
else if (start >= 0) { runs.push([start /
|
|
142
|
+
for (let x = 0; x <= fine; x++) {
|
|
143
|
+
const alpha = x < fine ? data[(y * fine + x) * 4 + 3] : 0;
|
|
144
|
+
if (x < fine && alpha < 250) opaque = false;
|
|
145
|
+
if (x < fine && alpha > alphaThreshold * 255) { if (start < 0) start = x; }
|
|
146
|
+
else if (start >= 0) { runs.push([start / fine, x / fine]); start = -1; }
|
|
138
147
|
}
|
|
139
148
|
bands.push(runs);
|
|
140
149
|
}
|
|
141
150
|
if (opaque) return null;
|
|
142
|
-
const runsAt = t => bands[Math.max(0, Math.min(
|
|
151
|
+
const runsAt = t => bands[Math.max(0, Math.min(fine - 1, Math.floor(t * fine)))];
|
|
143
152
|
return {runsAt,
|
|
144
153
|
spanAt(t) { const runs = runsAt(t); return runs.length ? [runs[0][0], runs.at(-1)[1]] : null; }};
|
|
145
154
|
}
|
|
@@ -234,15 +243,25 @@ export function parseProfile(text) {
|
|
|
234
243
|
return bandsProfile(bands);
|
|
235
244
|
}
|
|
236
245
|
|
|
246
|
+
// The standoff is round: a band's own rows are pushed out by the whole gap, and the rows above and below it, out to a gap away,
|
|
247
|
+
// by what a circle of that radius allows at their distance, in three steps. A square standoff stood up to 14 px off a slanted
|
|
248
|
+
// stroke and 10 off a level one; this stands 10 off both, within a step.
|
|
249
|
+
const shoulderSteps = 3, shoulders = Array.from({length: shoulderSteps}, (_, k) => ({from: k / shoulderSteps, to: (k + 1) / shoulderSteps, reach: Math.sqrt(1 - ((k + .5) / shoulderSteps) ** 2)}));
|
|
250
|
+
|
|
237
251
|
export function pictureSlices(profile, x, y, width, height, gap = 10) {
|
|
238
252
|
if (!profile) return [{x: x - gap, y: y - gap, width: width + gap * 2, height: height + gap * 2}];
|
|
239
|
-
|
|
253
|
+
// A band is at most 6 px high at the shown size, never coarser than the descriptor's 48 nor finer than the profile's 192.
|
|
254
|
+
const count = Math.max(shapeGrid, Math.min(sliceGridMax, Math.ceil((finite(height) && height > 0 ? height : 0) / slicePixelsPerBand)));
|
|
255
|
+
let byCount = profileBands.get(profile);
|
|
256
|
+
if (!byCount) profileBands.set(profile, byCount = new Map());
|
|
257
|
+
let bands = byCount.get(count);
|
|
240
258
|
if (!bands) {
|
|
241
259
|
bands = [];
|
|
242
260
|
let previous = new Map();
|
|
243
|
-
for (let index = 0; index <
|
|
244
|
-
const top = index /
|
|
245
|
-
|
|
261
|
+
for (let index = 0; index < count; index++) {
|
|
262
|
+
const top = index / count, bottom = (index + 1) / count;
|
|
263
|
+
// Sampled just inside the bottom edge: on the edge itself a band profile answers with the next band's row and the slice runs a row long.
|
|
264
|
+
const runs = [top, (top + bottom) / 2, bottom - 1e-9].flatMap(t => {
|
|
246
265
|
if (profile.runsAt) return profile.runsAt(t) || [];
|
|
247
266
|
const span = profile.spanAt(t);
|
|
248
267
|
return span ? [span] : [];
|
|
@@ -264,10 +283,19 @@ export function pictureSlices(profile, x, y, width, height, gap = 10) {
|
|
|
264
283
|
}
|
|
265
284
|
previous = current;
|
|
266
285
|
}
|
|
267
|
-
|
|
286
|
+
byCount.set(count, bands);
|
|
287
|
+
}
|
|
288
|
+
const slices = [];
|
|
289
|
+
for (const band of bands) {
|
|
290
|
+
const left = x + band.left * width, right = x + band.right * width, top = y + band.top * height, bottom = y + band.bottom * height;
|
|
291
|
+
slices.push({x: left - gap, y: top, width: right - left + gap * 2, height: bottom - top});
|
|
292
|
+
for (const {from, to, reach} of shoulders) {
|
|
293
|
+
const ext = gap * reach, span = gap * (to - from);
|
|
294
|
+
slices.push({x: left - ext, y: top - gap * to, width: right - left + ext * 2, height: span});
|
|
295
|
+
slices.push({x: left - ext, y: bottom + gap * from, width: right - left + ext * 2, height: span});
|
|
296
|
+
}
|
|
268
297
|
}
|
|
269
|
-
return
|
|
270
|
-
width: (band.right - band.left) * width + gap * 2, height: (band.bottom - band.top) * height + gap * 2}));
|
|
298
|
+
return slices;
|
|
271
299
|
}
|
|
272
300
|
|
|
273
301
|
export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -128,6 +128,52 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
128
128
|
return index;
|
|
129
129
|
}
|
|
130
130
|
export function documentAssets(source, factory) { return parseAssets(source, factory, markdownBodyOffset(source)); }
|
|
131
|
+
|
|
132
|
+
// The same index folded from the document's top-level blocks: `rows` are the blocks as markdown-it's block parse
|
|
133
|
+
// of the body cuts them, each {start, raw} with `start` absolute in `source`, and `parse(raw, row)` is parseAssets
|
|
134
|
+
// over one block (the caller may cache it by the row). A definition the whole parse finds is one a block's own parse
|
|
135
|
+
// finds at the block's place, because a top-level block's lines parse the same alone as in the document; labels,
|
|
136
|
+
// duplicates, the first definition's precedence, the count limit and the appendix fold as parseAssets folds them.
|
|
137
|
+
// Null when the rows are not this source: a block's bytes elsewhere than its start, rows out of order or overlapping,
|
|
138
|
+
// or text that could hold a definition (`]:`) between the body's start, the rows and the end, where no row parses
|
|
139
|
+
// it. Positions are the source's own, never normalized.
|
|
140
|
+
export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), bodyOffset = markdownBodyOffset(source)) {
|
|
141
|
+
if (typeof source !== 'string' || !Array.isArray(rows)) return null;
|
|
142
|
+
const blocks = [], references = Object.create(null), duplicateLabels = new Set(), seen = new Set(), assets = new Map();
|
|
143
|
+
let cursor = bodyOffset;
|
|
144
|
+
for (const row of rows) {
|
|
145
|
+
const raw = String(row.raw || ''), start = row.start;
|
|
146
|
+
if (!(start >= cursor) || source.slice(cursor, start).includes(']:') || source.slice(start, start + raw.length) !== raw) return null;
|
|
147
|
+
cursor = start + raw.length;
|
|
148
|
+
if (!raw.includes(']:')) continue;
|
|
149
|
+
const index = parse(raw, row);
|
|
150
|
+
// Every label the block defines, picture or not: an earlier definition of a label, of any kind, is the one
|
|
151
|
+
// the document resolves, so a later picture under it is inactive and the label a duplicate.
|
|
152
|
+
const labels = Object.keys(index.references);
|
|
153
|
+
for (const label of index.duplicateLabels) duplicateLabels.add(label);
|
|
154
|
+
for (const label of labels) {
|
|
155
|
+
if (seen.has(label)) duplicateLabels.add(label);
|
|
156
|
+
if (!Object.hasOwn(references, label)) references[label] = index.references[label];
|
|
157
|
+
}
|
|
158
|
+
for (const block of index.blocks) {
|
|
159
|
+
const active = block.active && !seen.has(block.id);
|
|
160
|
+
const shifted = {...block, start: block.start + start, end: block.end + start, status: 'unverified', active};
|
|
161
|
+
for (const key of ['urlStart', 'urlEnd', 'payloadStart', 'payloadEnd']) if (block[key] != null) shifted[key] = block[key] + start;
|
|
162
|
+
blocks.push(shifted);
|
|
163
|
+
if (active) assets.set(block.id, shifted);
|
|
164
|
+
}
|
|
165
|
+
for (const label of labels) seen.add(label);
|
|
166
|
+
}
|
|
167
|
+
if (source.slice(cursor).includes(']:')) return null;
|
|
168
|
+
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
169
|
+
let appendixStart = source.length;
|
|
170
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
171
|
+
const block = blocks[index];
|
|
172
|
+
if (!block.active || !block.topLevel || !/^[ \t\r\n]*$/.test(source.slice(block.end, appendixStart))) break;
|
|
173
|
+
appendixStart = block.start;
|
|
174
|
+
}
|
|
175
|
+
return {assets, blocks, duplicateLabels, appendixStart, references};
|
|
176
|
+
}
|
|
131
177
|
export function imageEnvironment(source, factory) { return {references: Object.assign(Object.create(null), documentAssets(source, factory).references)}; }
|
|
132
178
|
|
|
133
179
|
function imageUses(source, factory, env = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
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
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
--md-text-base:1rem;
|
|
17
17
|
--md-fw-semibold:600;
|
|
18
18
|
--md-fw-regular:400;
|
|
19
|
-
--md-
|
|
19
|
+
--md-text-body:1.1rem;
|
|
20
|
+
--md-line:1.75rem;
|
|
20
21
|
--md-space-6:24px;
|
|
21
22
|
--md-space-4:16px;
|
|
22
23
|
--md-space-3:12px;
|
|
@@ -31,11 +32,10 @@
|
|
|
31
32
|
--md-color-border-focus:light-dark(#c4c2bc,#2a2a2a);
|
|
32
33
|
--md-color-text-secondary:light-dark(#444444,#a1a1a1);
|
|
33
34
|
--md-color-text-muted:light-dark(#606060,#878787);
|
|
34
|
-
--md-color-success:light-dark(#
|
|
35
|
-
--md-color-warning:light-dark(#
|
|
35
|
+
--md-color-success:light-dark(#188844,#45d483);
|
|
36
|
+
--md-color-warning:light-dark(#b26105,#f5a623);
|
|
36
37
|
--md-color-error:light-dark(#c00,#ec5156);
|
|
37
38
|
--md-space-8:32px;
|
|
38
|
-
--md-space-7:28px;
|
|
39
39
|
--md-color-accent:#12A594;
|
|
40
40
|
--md-color-text-subtle:light-dark(#636363,#858585);
|
|
41
41
|
--md-heading-color:var(--md-color-text);
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
runs past the column reads as a wide table does -- it scrolls, under the same fading edge. */
|
|
118
118
|
:where(.md-render) .diagram-block {
|
|
119
119
|
position: relative;
|
|
120
|
-
margin:
|
|
120
|
+
margin: 0 0 var(--md-line);
|
|
121
121
|
max-width: 100%;
|
|
122
122
|
overflow: auto;
|
|
123
123
|
}
|
|
@@ -345,25 +345,31 @@
|
|
|
345
345
|
opacity: .78;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/* The line model (item 2c, the founder's: the way a Word document reads). One line, --md-line, is the body's line
|
|
349
|
+
box: 1.75rem, 1.6 lines of the 1.1rem body, a whole 28 px at the browser's 16 px root so baselines land on pixels.
|
|
350
|
+
Every block ends one line below its last line; a heading ends half a line below. A heading's own box is the whole
|
|
351
|
+
or half lines its size fills: h1 at 2.4 x the body on two lines, h2 and h3 on one and a half, h4 to h6 on one.
|
|
352
|
+
Nothing sets a top margin, so a plain page (sibling margins collapsing) and the editor (a wrapper per block) space
|
|
353
|
+
alike, and the space before any block is the line its predecessor leaves. */
|
|
348
354
|
.md-render{
|
|
349
|
-
font:var(--md-fw-regular)
|
|
355
|
+
font:var(--md-fw-regular) var(--md-text-body)/var(--md-line) var(--md-font-sans);color:var(--md-color-text);text-align:start}
|
|
350
356
|
.md-render>:first-child,.md-render :is(blockquote,li,td,th,dd,.callout,details)>:first-child{margin-top:0}
|
|
357
|
+
/* A block's last line ends the block: the line its last paragraph would leave is the block's own (a quote used to stand a line taller than its words). */
|
|
358
|
+
.md-render :is(blockquote,td,th,dd)>:last-child{margin-bottom:0}
|
|
351
359
|
.md-render h1,.md-render h2,.md-render h3,.md-render h4,.md-render h5,.md-render h6{
|
|
352
360
|
font-family:var(--md-font-sans);letter-spacing:-0.01em;
|
|
353
|
-
line-height:var(--md-
|
|
354
|
-
.md-render h1{font-size:2.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.md-render
|
|
358
|
-
.md-render h3{font-size:1.7rem;font-weight:600;margin:var(--md-space-5) 0 var(--md-space-2)}
|
|
359
|
-
.md-render h4{font-size:1.3rem;font-weight:500;margin:var(--md-space-4) 0 var(--md-space-2)}
|
|
361
|
+
line-height:var(--md-line);margin:0 0 calc(var(--md-line) / 2);color:var(--md-heading-color);text-wrap:balance}
|
|
362
|
+
.md-render h1{font-size:calc(var(--md-text-body) * 2.4);line-height:calc(var(--md-line) * 2);font-weight:700}
|
|
363
|
+
.md-render h2{font-size:calc(var(--md-text-body) * 1.9);line-height:calc(var(--md-line) * 1.5);font-weight:600}
|
|
364
|
+
.md-render h3{font-size:calc(var(--md-text-body) * 1.5);line-height:calc(var(--md-line) * 1.5);font-weight:600}
|
|
365
|
+
.md-render h4{font-size:calc(var(--md-text-body) * 1.25);font-weight:500}
|
|
360
366
|
/* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/colour), never
|
|
361
|
-
the browser default. Size stays strictly monotonic below h4; h6
|
|
362
|
-
since its size alone
|
|
363
|
-
.md-render h5{font-size:
|
|
364
|
-
.md-render h6{font-size:
|
|
365
|
-
.md-render p{margin:0 0 var(--md-
|
|
366
|
-
.md-render ul,.md-render ol{margin:0 0 var(--md-
|
|
367
|
+
the browser default. Size stays strictly monotonic below h4; h6 sits at the body's own size and
|
|
368
|
+
firms up weight and dims colour, since its size alone would read as body text. */
|
|
369
|
+
.md-render h5{font-size:calc(var(--md-text-body) * 1.1);font-weight:600}
|
|
370
|
+
.md-render h6{font-size:var(--md-text-body);font-weight:600;color:var(--md-heading-muted-color)}
|
|
371
|
+
.md-render p{margin:0 0 var(--md-line)}
|
|
372
|
+
.md-render ul,.md-render ol{margin:0 0 var(--md-line);padding-inline-start:var(--md-space-5)}
|
|
367
373
|
.md-render ul{list-style:none}
|
|
368
374
|
.md-render ul>li{position:relative}
|
|
369
375
|
|
|
@@ -445,7 +451,7 @@
|
|
|
445
451
|
.md-render ol{
|
|
446
452
|
list-style:none;
|
|
447
453
|
padding-inline-start:2.75rem;
|
|
448
|
-
margin:0 0 var(--md-
|
|
454
|
+
margin:0 0 var(--md-line);
|
|
449
455
|
counter-reset:rapier-ol;
|
|
450
456
|
}
|
|
451
457
|
.md-render ol>li{
|
|
@@ -475,12 +481,13 @@
|
|
|
475
481
|
|
|
476
482
|
inset-inline-start:calc(-1.75rem - 1.5px);
|
|
477
483
|
top:2.2rem;
|
|
478
|
-
bottom
|
|
484
|
+
bottom:calc(var(--md-line) / -4);
|
|
479
485
|
border-inline-start:3px dotted var(--md-color-line);
|
|
480
486
|
z-index:0;
|
|
481
487
|
}
|
|
482
|
-
.md-render li{margin:0 0 var(--md-
|
|
483
|
-
.md-render li
|
|
488
|
+
.md-render li{margin:0 0 calc(var(--md-line) / 4)}
|
|
489
|
+
.md-render li:last-child{margin-bottom:0}
|
|
490
|
+
.md-render li>ul,.md-render li>ol{margin:calc(var(--md-line) / 4) 0 0}
|
|
484
491
|
.md-render a{color:var(--md-color-accent-text);text-decoration:underline;text-underline-offset:2px}
|
|
485
492
|
/* The house focus (rapier-app.css) in the document and on the exported page: the underline thickens, no ring. */
|
|
486
493
|
.md-render a:focus-visible,.md-render ul.rapier-jump-list>li>a:focus-visible,.md-render .footnote-ref a:focus-visible,.md-render summary:focus-visible{outline:none;text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:.2em}
|
|
@@ -499,7 +506,7 @@
|
|
|
499
506
|
--md-jump-marker-center:1em;
|
|
500
507
|
list-style:none;
|
|
501
508
|
padding-inline-start:1.85rem;
|
|
502
|
-
margin:0 0 var(--md-
|
|
509
|
+
margin:0 0 var(--md-line);
|
|
503
510
|
}
|
|
504
511
|
.md-render ul.rapier-jump-list>li{
|
|
505
512
|
position:relative;
|
|
@@ -543,13 +550,13 @@
|
|
|
543
550
|
.md-render em{font-style:italic}
|
|
544
551
|
.md-render code{font-family:var(--md-font-mono);font-size:0.85em;
|
|
545
552
|
background:var(--md-color-inset);border:1px solid var(--md-color-border);padding:1px 5px}
|
|
546
|
-
.md-render pre{margin:0 0 var(--md-
|
|
553
|
+
.md-render pre{margin:0 0 var(--md-line);padding:var(--md-space-3) var(--md-space-4);
|
|
547
554
|
background:var(--md-color-surface-2);border:1px solid var(--md-color-border);overflow-x:auto}
|
|
548
|
-
.md-render pre code{background:none;border:none;padding:0;font-size:0.8125rem;line-height:
|
|
555
|
+
.md-render pre code{background:none;border:none;padding:0;font-size:0.8125rem;line-height:calc(var(--md-line) * .75)}
|
|
549
556
|
.md-render .math-display-wrap{
|
|
550
557
|
display:block;
|
|
551
558
|
max-width:100%;
|
|
552
|
-
margin:0 0 var(--md-
|
|
559
|
+
margin:0 0 var(--md-line);
|
|
553
560
|
overflow-x:auto;
|
|
554
561
|
overflow-y:hidden;
|
|
555
562
|
-webkit-overflow-scrolling:touch;
|
|
@@ -565,14 +572,14 @@
|
|
|
565
572
|
text-align:initial;
|
|
566
573
|
vertical-align:middle;
|
|
567
574
|
}
|
|
568
|
-
.md-render p>.math-display-wrap{margin:var(--md-
|
|
575
|
+
.md-render p>.math-display-wrap{margin:calc(var(--md-line) / 4) 0 var(--md-line)}
|
|
569
576
|
.md-render math[display="block"]{font-size:1.12em}
|
|
570
577
|
.md-render math:not([display="block"]){font-size:1em}
|
|
571
|
-
.md-render blockquote{margin:0 0 var(--md-
|
|
578
|
+
.md-render blockquote{margin:0 0 var(--md-line);
|
|
572
579
|
padding-block:var(--md-space-1);padding-inline-start:var(--md-space-4);padding-inline-end:0;
|
|
573
580
|
border-inline-start:2px solid var(--md-color-border-focus);color:var(--md-color-text-secondary)}
|
|
574
581
|
.md-render blockquote blockquote{
|
|
575
|
-
margin:var(--md-
|
|
582
|
+
margin:calc(var(--md-line) / 2) 0 calc(var(--md-line) / 4);padding-inline-start:var(--md-space-3);
|
|
576
583
|
border-inline-start-color:color-mix(in srgb,var(--md-color-border-focus) 70%,var(--md-color-text-muted));
|
|
577
584
|
color:color-mix(in srgb,var(--md-color-text-secondary) 84%,var(--md-color-text-muted))
|
|
578
585
|
}
|
|
@@ -597,7 +604,7 @@
|
|
|
597
604
|
.md-render blockquote.callout>:last-child{margin-bottom:0}
|
|
598
605
|
@media print{.md-render{--md-callout-note:#0969da;--md-callout-important:#8250df}}
|
|
599
606
|
|
|
600
|
-
.md-render details { border: 1px solid var(--md-color-border); border-radius: 0; padding: var(--md-space-2) var(--md-space-4); margin: 0 0 var(--md-
|
|
607
|
+
.md-render details { border: 1px solid var(--md-color-border); border-radius: 0; padding: var(--md-space-2) var(--md-space-4); margin: 0 0 var(--md-line); }
|
|
601
608
|
.md-render details[open] { padding-bottom: var(--md-space-3); }
|
|
602
609
|
/* A section with nothing typed into it yet has no paragraph to tap: without this, the only body
|
|
603
610
|
space below the summary is the plain padding-bottom above (12px), too thin a margin for a real
|
|
@@ -615,15 +622,15 @@
|
|
|
615
622
|
background:transparent;
|
|
616
623
|
color:var(--md-color-line);
|
|
617
624
|
opacity:1;
|
|
618
|
-
margin:var(--md-
|
|
625
|
+
margin:var(--md-line) 0;
|
|
619
626
|
overflow:visible;
|
|
620
627
|
}
|
|
621
628
|
.md-render hr[data-hr-style="dash"]{
|
|
622
629
|
border-top:2px solid var(--md-color-line);
|
|
623
630
|
}
|
|
624
631
|
.md-render hr[data-hr-style="stars"]{
|
|
625
|
-
height:
|
|
626
|
-
margin:var(--md-
|
|
632
|
+
height:var(--md-line);
|
|
633
|
+
margin:var(--md-line) 0;
|
|
627
634
|
}
|
|
628
635
|
.md-render hr[data-hr-style="stars"]::before{
|
|
629
636
|
content:"";
|
|
@@ -652,7 +659,7 @@
|
|
|
652
659
|
height:0;
|
|
653
660
|
border:none;
|
|
654
661
|
border-top:1px dashed var(--md-color-text-muted);
|
|
655
|
-
margin:var(--md-
|
|
662
|
+
margin:var(--md-line) 0;
|
|
656
663
|
opacity:.37;
|
|
657
664
|
}
|
|
658
665
|
.md-render [data-md-break]::after{
|
|
@@ -671,7 +678,7 @@
|
|
|
671
678
|
.md-render table{
|
|
672
679
|
width:100%;
|
|
673
680
|
border-collapse:collapse;
|
|
674
|
-
margin:0 0 var(--md-
|
|
681
|
+
margin:0 0 var(--md-line);
|
|
675
682
|
font-size:var(--md-text-sm);
|
|
676
683
|
border:2px solid var(--md-color-line);
|
|
677
684
|
}
|
|
@@ -695,7 +702,7 @@
|
|
|
695
702
|
adjacent-sibling rule below settles the real "is it truly right after a table" question against
|
|
696
703
|
the live .block-wrapper DOM, reactively, with no bookkeeping kept in sync on insert/delete/reorder. */
|
|
697
704
|
:where(.md-render) .rapier-table-caption {
|
|
698
|
-
margin-top: calc(-1
|
|
705
|
+
margin-top: calc(var(--md-space-1) - var(--md-line));
|
|
699
706
|
font-style: italic;
|
|
700
707
|
font-size: var(--md-text-sm);
|
|
701
708
|
color: var(--md-color-text-secondary);
|
|
@@ -714,8 +721,8 @@
|
|
|
714
721
|
.md-render :has(>img[data-md-drawing],>a>img[data-md-drawing]){overflow-x:auto}
|
|
715
722
|
.md-render img:is([data-rapier-asset],[data-rapier-image-url]):not([data-rapier-asset-state="ready"]){background:color-mix(in srgb,var(--md-color-text) 5%,transparent)}
|
|
716
723
|
|
|
717
|
-
.md-render dl{margin:0 0 var(--md-
|
|
718
|
-
.md-render dt{font-weight:var(--md-fw-semibold);margin:var(--md-
|
|
724
|
+
.md-render dl{margin:0 0 var(--md-line)}
|
|
725
|
+
.md-render dt{font-weight:var(--md-fw-semibold);margin:calc(var(--md-line) / 4) 0 0}
|
|
719
726
|
.md-render dt:first-child{margin-top:0}
|
|
720
727
|
.md-render dd{margin:var(--md-space-1) 0 0;padding-inline-start:var(--md-space-4);color:var(--md-color-text-secondary)}
|
|
721
728
|
|
|
@@ -752,7 +759,7 @@
|
|
|
752
759
|
.md-render del,.md-render s{color:var(--md-color-text-muted);text-decoration:line-through}
|
|
753
760
|
|
|
754
761
|
.md-render .footnotes-sep{display:none}
|
|
755
|
-
.md-render .footnotes{margin:var(--md-
|
|
762
|
+
.md-render .footnotes{margin:var(--md-line) 0 0;padding-top:var(--md-space-3);border-top:1px solid var(--md-color-border);font-size:var(--md-text-sm);color:var(--md-color-text-secondary)}
|
|
756
763
|
.md-render .footnotes-list{list-style:decimal;padding-inline-start:var(--md-space-5);margin:0;counter-reset:none}
|
|
757
764
|
.md-render .footnotes-list>li{padding-top:0;min-height:0}
|
|
758
765
|
.md-render .footnotes-list>li::before,.md-render .footnotes-list>li::after{content:none}
|
|
@@ -781,8 +788,9 @@
|
|
|
781
788
|
}
|
|
782
789
|
|
|
783
790
|
/* Overflow belongs to the document; the surrounding page owns its measure and padding. */
|
|
784
|
-
|
|
785
|
-
.md-render .table-scroll-wrap
|
|
791
|
+
/* A wide table scrolls inside its wrap; the wrap carries the block's line, so a scrollbar sits under the table, not a line below it. */
|
|
792
|
+
.md-render .table-scroll-wrap{width:100%;margin:0 0 var(--md-line);overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
|
|
793
|
+
.md-render .table-scroll-wrap table{min-width:100%;margin-bottom:0}
|
|
786
794
|
@media print{
|
|
787
795
|
.md-render [data-md-break]{border:0!important;height:0!important;margin:0!important;break-before:page;page-break-before:always}
|
|
788
796
|
.md-render [data-md-break]::after{content:none!important}
|