rapier-markdown-kit 1.1.64 → 1.1.73
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 +1 -1
- package/dist/kit/conformance/README.md +6 -6
- package/dist/kit/ledger/journal-records.mjs +5 -6
- package/dist/kit/ledger/merge.mjs +15 -0
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-markdown.mjs +14 -13
- package/dist/kit/render-sanitize.mjs +32 -13
- package/dist/kit/render-styles.mjs +1 -1
- package/dist/kit/render.mjs +64 -66
- package/dist/layout/model.mjs +18 -11
- package/dist/spec/md-assets.mjs +1 -1
- package/dist/spec/md-layout.mjs +5 -5
- package/dist/spec/md-marks.mjs +9 -9
- package/package.json +1 -1
- package/style.css +66 -62
package/dist/agent/will.mjs
CHANGED
|
@@ -48,7 +48,7 @@ function _rapierWillMarkerOf(content) {
|
|
|
48
48
|
if (!intent.length || intent.indexOf('--') >= 0) return near('malformed_marker');
|
|
49
49
|
if ([...intent].length > _RAPIER_WILL_INTENT_LIMIT) return near('intent_over_bound');
|
|
50
50
|
}
|
|
51
|
-
// The words after the colon are document data, never permission
|
|
51
|
+
// The words after the colon are document data, never permission; willGovern alone decides.
|
|
52
52
|
return { kind: 'open', law: word, intent, content: line };
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -31,7 +31,7 @@ anchor; that historical measurement is not a normative neighbour-selection expec
|
|
|
31
31
|
alpha shape of the fixture's own picture, computed independently in Node from how the picture was
|
|
32
32
|
built (see the generation notes at the foot of this file) — not read back from Rapier — since these
|
|
33
33
|
are plain rasters/foreign SVGs and Rapier's export only serializes `data-rapier-occupancy` for its
|
|
34
|
-
own drawing recipes
|
|
34
|
+
own drawing recipes.
|
|
35
35
|
- `lines` — the anchor's own line boxes (`left`/`top`/`right`/`bottom`), relative to its own content
|
|
36
36
|
box, read from Rapier's styled export: from the reflow script's own `<span>` boxes when the
|
|
37
37
|
paragraph is flowed (their `right` is the reserved *slot*'s edge, `x + width` — the script sizes
|
|
@@ -40,8 +40,8 @@ anchor; that historical measurement is not a normative neighbour-selection expec
|
|
|
40
40
|
*is* the glyphs' own tight extent).
|
|
41
41
|
- `glyphWidths` — every character the anchor's text uses, with its own measured width in that exact
|
|
42
42
|
font, from this browser's `measureText`, called in isolation on one character at a time. Always
|
|
43
|
-
present; the fallback `measurer.mjs`
|
|
44
|
-
- `measurements`
|
|
43
|
+
present; the fallback `measurer.mjs` reaches for when a query is not in `measurements` below.
|
|
44
|
+
- `measurements` — every `(font, text)` pair Pretext's own `getMeasureContext()` was actually
|
|
45
45
|
asked to measure while *this fixture's own export* ran its real reflow script, each with the exact
|
|
46
46
|
width the browser gave it — captured from the exported page's own reflow as it ran.
|
|
47
47
|
This is Rapier's own measurement, not a stand-in: a caller replaying the same font and text later
|
|
@@ -64,7 +64,7 @@ anchor; that historical measurement is not a normative neighbour-selection expec
|
|
|
64
64
|
## Running the comparison
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
|
-
node run.mjs # the kit itself, driven by measurer.mjs
|
|
67
|
+
node run.mjs # the kit itself, driven by measurer.mjs
|
|
68
68
|
node run.mjs --impl path/to/other-kit/index.mjs --tolerance 4
|
|
69
69
|
```
|
|
70
70
|
|
|
@@ -74,8 +74,8 @@ Prints two separate tables (preservation and presentation are different claims):
|
|
|
74
74
|
`parseLayout`/`formatLayout`. No measurer, no font: either exactly right, or a real bug. 12/12.
|
|
75
75
|
- **Presentation** — does the planned line count and each line's left/right agree with the recorded
|
|
76
76
|
fact, within `--tolerance` pixels (default 2). This depends entirely on the measurer supplied;
|
|
77
|
-
`run.mjs`'s own default (`measurer.mjs
|
|
78
|
-
captured, falling back to the character-glyph sum only where none exists. Recorded
|
|
77
|
+
`run.mjs`'s own default (`measurer.mjs`) replays Rapier's real Pretext trace wherever one was
|
|
78
|
+
captured, falling back to the character-glyph sum only where none exists. Recorded result (not a current-run receipt): **3/13
|
|
79
79
|
AGREE outright** (`04-wrap-box`, `10-both-sides`, `12-heading-barrier` — every
|
|
80
80
|
one a case where the compared edge is pure obstacle-slot geometry, unaffected by text fidelity
|
|
81
81
|
either way), **10 CLOSE, 0 DIFFERS**, each CLOSE row's cause named rather than left as "close
|
|
@@ -45,12 +45,11 @@ function _rapierRecordSplices(entry, ledger) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// A ledger record as the journal and the Notes return validate it below: plain data, nothing
|
|
48
|
-
// undefined anywhere in it. A live record could carry a row whose type or order
|
|
49
|
-
//
|
|
50
|
-
// the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// so it never goes stale.
|
|
48
|
+
// undefined anywhere in it. A live record could carry a row whose type or order is not yet known (a
|
|
49
|
+
// block just inserted, before its first render: `type: undefined`), and the validator would refuse
|
|
50
|
+
// the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and
|
|
51
|
+
// as "Rapier could not be reopened: the document history could not be restored" on a return from
|
|
52
|
+
// Notes. The copy is made once per record; records are frozen, so it never goes stale.
|
|
54
53
|
const _rapierJournalRecord = (() => {
|
|
55
54
|
const copies = new WeakMap();
|
|
56
55
|
return record => {
|
|
@@ -16,6 +16,21 @@ export function transportInterval(start, end, splices) {
|
|
|
16
16
|
}
|
|
17
17
|
return {start: a, end: b};
|
|
18
18
|
}
|
|
19
|
+
// Follow the current extent of a touched range for disclosure or the person's focus.
|
|
20
|
+
// Unlike transportInterval, this does not prove unchanged source and never grants edit authority.
|
|
21
|
+
export function transportTouchedInterval(start, end, splices) {
|
|
22
|
+
const unchanged = transportInterval(start, end, splices);
|
|
23
|
+
if (unchanged) return unchanged;
|
|
24
|
+
let range = {start, end};
|
|
25
|
+
for (const row of splices) {
|
|
26
|
+
// A split replacement can insert at a boundary before another splice touches the range.
|
|
27
|
+
const boundaryInsertion = !row.removed.length && (row.pos === range.start || row.pos === range.end);
|
|
28
|
+
const exact = boundaryInsertion ? null : transportInterval(range.start, range.end, [row]);
|
|
29
|
+
range = exact || {start: Math.min(range.start, row.pos),
|
|
30
|
+
end: Math.max(row.pos + row.inserted.length, range.end + row.inserted.length - row.removed.length)};
|
|
31
|
+
}
|
|
32
|
+
return range;
|
|
33
|
+
}
|
|
19
34
|
function over(row, other) {
|
|
20
35
|
// Equal-position insertions have no ordering evidence. Review, never a lexical winner.
|
|
21
36
|
if (!row.removed.length && !other.removed.length && row.pos === other.pos) return null;
|
|
@@ -3,7 +3,7 @@ import {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, histor
|
|
|
3
3
|
export {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, historyEnvelope, agentActorId};
|
|
4
4
|
import {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
|
|
5
5
|
export {authorship, readAuthorship, ledgerFromAuthorship};
|
|
6
|
-
import {merge, transportInterval} from './ledger/merge.mjs';
|
|
7
|
-
export {merge, transportInterval};
|
|
6
|
+
import {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
|
|
7
|
+
export {merge, transportInterval, transportTouchedInterval};
|
|
8
8
|
import {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord} from './ledger/journal-records.mjs';
|
|
9
9
|
export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord};
|
|
@@ -6,12 +6,12 @@ function createMarkdownRenderer(runtime) {
|
|
|
6
6
|
let md = runtime.md;
|
|
7
7
|
function initMarkdownIt() {
|
|
8
8
|
if (typeof window.markdownit !== 'function') throw new Error('markdownit');
|
|
9
|
-
if (typeof _rapierMarkdownPreview === 'function') globalThis.rapierRenderPreview = _rapierMarkdownPreview; // Notes' cards read through here
|
|
9
|
+
if (typeof _rapierMarkdownPreview === 'function') globalThis.rapierRenderPreview = _rapierMarkdownPreview; // Notes' cards read through here; the Node witnesses that run this function alone have no such neighbour
|
|
10
10
|
|
|
11
11
|
md = window.markdownit({
|
|
12
12
|
...RAPIER_MARKDOWN_SPEC.options,
|
|
13
13
|
// The first paint is coloured: the CPU reading, synchronously, for every admitted fence.
|
|
14
|
-
highlight: (code, lang) => _rapierHighlightAdmitted(code) ? _rapierCodeHtml(code, lang) : '',
|
|
14
|
+
highlight: (code, lang) => lang && _rapierHighlightAdmitted(code) ? _rapierCodeHtml(code, lang) : '',
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
_rapierApplyMarkdownSpec(md, window);
|
|
@@ -51,9 +51,9 @@ function initMarkdownIt() {
|
|
|
51
51
|
return '<span data-md-color="' + hex + '" style="--md-color:' + hex + ';--md-color-dark:' + dark + '">';
|
|
52
52
|
};
|
|
53
53
|
md.renderer.rules.rapier_color_close = () => '</span>';
|
|
54
|
-
// Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
|
|
55
|
-
// close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes
|
|
56
|
-
//
|
|
54
|
+
// Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
|
|
55
|
+
// comment's close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes.
|
|
56
|
+
// The span itself shows nothing.
|
|
57
57
|
md.renderer.rules.rapier_ink_open = (tokens, idx) => {
|
|
58
58
|
const body = String(tokens[idx].attrGet('data-rapier-ink') || '');
|
|
59
59
|
// The dark-page colour on the span, as the colour span's: an exported page draws its ink with no engine to ask (the
|
|
@@ -111,7 +111,7 @@ function initMarkdownIt() {
|
|
|
111
111
|
const alt = suffix && semanticAlt.endsWith(suffix) ? semanticAlt.slice(0, -suffix.length) : semanticAlt;
|
|
112
112
|
const layout = token.meta?.mdLayout?.imageOnly ? token.meta.mdLayout.marker : '';
|
|
113
113
|
const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
|
|
114
|
-
// A connected host's own asset
|
|
114
|
+
// A connected host's own asset; a host without one (the server) supplies no port.
|
|
115
115
|
if (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml')) return globalThis.RapierEmbeddedImages.imageHtml(
|
|
116
116
|
reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
|
|
117
117
|
if (!_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
|
|
@@ -301,21 +301,22 @@ function initMarkdownIt() {
|
|
|
301
301
|
let _mathPromptedOnce = false;
|
|
302
302
|
const _renderMath = (src, displayMode) => {
|
|
303
303
|
const marker = displayMode ? '$$' : '$';
|
|
304
|
+
const source = marker + src + marker, literal = md.utils.escapeHtml(source);
|
|
304
305
|
const mathProvider = _rapierProviders.math;
|
|
305
306
|
if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
|
|
306
307
|
if (!_mathPromptedOnce) {
|
|
307
308
|
_mathPromptedOnce = true;
|
|
308
309
|
try { _rapierUiMath.request(); } catch (_) {}
|
|
309
310
|
}
|
|
310
|
-
return '<span class="math-placeholder" title="Install the math plug-in in Settings to show this equation.">'
|
|
311
|
-
+
|
|
311
|
+
return '<span class="math-placeholder" data-rapier-math-source="" title="Install the math plug-in in Settings to show this equation.">'
|
|
312
|
+
+ literal + '</span>';
|
|
312
313
|
}
|
|
313
314
|
try {
|
|
314
315
|
const rendered = mathProvider.renderToString(src, { displayMode });
|
|
315
316
|
return '<span class="math-rendered" data-math-src="'
|
|
316
|
-
+ encodeURIComponent(
|
|
317
|
+
+ encodeURIComponent(source) + '">' + rendered + '</span>';
|
|
317
318
|
}
|
|
318
|
-
catch (_) { return
|
|
319
|
+
catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
|
|
319
320
|
};
|
|
320
321
|
md.renderer.rules.math_inline = (tokens, idx) => _renderMath(tokens[idx].content, false);
|
|
321
322
|
md.renderer.rules.math_block = (tokens, idx) => wrapDisplayMath(_renderMath(tokens[idx].content, true)) + '\n';
|
|
@@ -675,9 +676,9 @@ function _rapierDisambiguateRenderedAnchors(root) {
|
|
|
675
676
|
}
|
|
676
677
|
|
|
677
678
|
function _rapierDecodeHash(hash) {
|
|
678
|
-
// A document's own address may carry a heading after it (`#d/<id>/<heading
|
|
679
|
-
//
|
|
680
|
-
//
|
|
679
|
+
// A document's own address may carry a heading after it (`#d/<id>/<heading>`): the heading is the
|
|
680
|
+
// anchor, and an address with no heading names nothing to jump to. A view's (`#v/draw`,
|
|
681
|
+
// `#v/notes`) names no heading either (docs/agents.md "The address of a document").
|
|
681
682
|
let raw = String(hash || '').replace(/^#/, '');
|
|
682
683
|
const own = /^[dnv]\/[^/]*(?:\/(.*))?$/.exec(raw);
|
|
683
684
|
if (own) raw = own[1] || '';
|
|
@@ -23,6 +23,26 @@ const RAPIER_SANITIZE_OPTIONS = Object.freeze({
|
|
|
23
23
|
FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
|
|
24
24
|
}),
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
// Snapshot current math text before a Markdown writer collapses whitespace. The parsed copy
|
|
28
|
+
// stays inert; the writer still admits raw source through its shared math grammar.
|
|
29
|
+
function prepareMathSource(input, DOMParser) {
|
|
30
|
+
const html = typeof input === 'string';
|
|
31
|
+
const MARK = 'span[data-rapier-math-source]';
|
|
32
|
+
if (html ? !/data-rapier-math-source/i.test(input) : typeof input?.cloneNode !== 'function' ||
|
|
33
|
+
!(input.matches?.(MARK) || input.querySelector?.(MARK))) return input;
|
|
34
|
+
const root = html ? new DOMParser().parseFromString(
|
|
35
|
+
'<x-turndown id="turndown-root">' + input + '</x-turndown>', 'text/html').getElementById('turndown-root') : input.cloneNode(true);
|
|
36
|
+
const nodes = Array.from(root.querySelectorAll(MARK));
|
|
37
|
+
const sourceRoot = root.nodeName === 'SPAN' && root.hasAttribute('data-rapier-math-source');
|
|
38
|
+
if (sourceRoot) nodes.push(root);
|
|
39
|
+
for (const node of nodes) {
|
|
40
|
+
try { node.setAttribute('data-rapier-math-source', encodeURIComponent(node.textContent || '')); }
|
|
41
|
+
catch (_) { node.removeAttribute('data-rapier-math-source'); }
|
|
42
|
+
}
|
|
43
|
+
return sourceRoot ? root.outerHTML : root;
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
27
47
|
function createRenderSanitizer(runtime) {
|
|
28
48
|
const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, _rapierVerifyRasterBytes, document, globalThis, location} = runtime;
|
|
@@ -39,15 +59,14 @@ function _rapierInstallSanitizeHooks() {
|
|
|
39
59
|
&& _rapierSafeRasterDataUrl(compact);
|
|
40
60
|
if (!rasterAllowed) data.keepAttr = false;
|
|
41
61
|
});
|
|
42
|
-
// Paste and render sanitise: a remote <img src> would fetch the instant its markup lands in the
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// converted markdown as its content.
|
|
62
|
+
// Paste and render sanitise: a remote <img src> would fetch the instant its markup lands in the page,
|
|
63
|
+
// so the src the hook below is about to strip never survives. Move it to a data-* attribute first
|
|
64
|
+
// (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
|
|
65
|
+
// either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
|
|
66
|
+
// pasted-page picture and write out `` -- not vanish -- and an <img> in a document's own
|
|
67
|
+
// HTML (a table kept as HTML) is written again with its URL: the raw profile admits no data-*, so
|
|
68
|
+
// only the three this hook sets on the node it holds back stay. A picture inside a link is the same
|
|
69
|
+
// node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
|
|
51
70
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
52
71
|
if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw)$/.test(_rapierSanitizeRuntime.context)) return;
|
|
53
72
|
if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
|
|
@@ -105,9 +124,9 @@ function _rapierInstallSanitizeHooks() {
|
|
|
105
124
|
data.forceKeepAttr = true;
|
|
106
125
|
}
|
|
107
126
|
});
|
|
108
|
-
// A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's
|
|
109
|
-
//
|
|
110
|
-
//
|
|
127
|
+
// A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's own
|
|
128
|
+
// click router already opens with noopener,noreferrer; the sanitized markup that Share and the
|
|
129
|
+
// standalone export carry gets the same rule in its bytes.
|
|
111
130
|
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
|
112
131
|
if (!node || !/^(?:A|AREA)$/i.test(node.nodeName) || !node.hasAttribute('target')) return;
|
|
113
132
|
if (String(node.getAttribute('target') || '').trim().toLowerCase() !== '_blank') return;
|
|
@@ -220,4 +239,4 @@ function _rapierDropRemoteRules(rules, owner) {
|
|
|
220
239
|
|
|
221
240
|
return {_rapierInstallSanitizeHooks, sanitizeRapierHtml, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
222
241
|
}
|
|
223
|
-
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, RAPIER_SANITIZE_OPTIONS as options};
|
|
242
|
+
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
|
@@ -18,7 +18,7 @@ body{
|
|
|
18
18
|
--color-text-subtle:var(--md-color-text-subtle);--color-accent-text:var(--md-color-accent-text);
|
|
19
19
|
margin:0;background:var(--md-color-bg);color:var(--md-color-text);font-family:var(--md-font-sans);-webkit-font-smoothing:antialiased
|
|
20
20
|
}
|
|
21
|
-
.rapier-page{width:min(100%,
|
|
21
|
+
.rapier-page{width:min(100%,calc(var(--md-measure,39.6rem) + 48px));margin:0 auto;padding:48px 24px 72px}
|
|
22
22
|
.rapier-page [id]{scroll-margin-top:var(--md-space-8)}
|
|
23
23
|
.artifact-flat{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
24
24
|
.artifact-flat--text{font:var(--md-fw-regular) 1rem/1.7 var(--md-font-sans)}
|
package/dist/kit/render.mjs
CHANGED
|
@@ -87,11 +87,9 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
87
87
|
// styles and other page resources get no network authority. Following an ordinary link
|
|
88
88
|
// is the reader's navigation, with no Referer, not a background resource request.
|
|
89
89
|
const nonce = _rapierArtifactNonce();
|
|
90
|
-
// The reflow script is written first, because whether it exists decides the policy. A page that
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// return caught this against the R86k fold, which always emitted the directive; the rule here
|
|
94
|
-
// is tighter than either that fold or G3's own writer, which could not carry a script at all.
|
|
90
|
+
// The reflow script is written first, because whether it exists decides the policy. A page that carries no script says so: script-src 'none' refuses every
|
|
91
|
+
// script outright. Declaring a nonce a page has no use for is a widening, small but real -- anything that could inject markup into the file could read the
|
|
92
|
+
// nonce out of it and be admitted.
|
|
95
93
|
const layoutScript = !opts.print ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
|
|
96
94
|
// The lexer for the page's code, under the same nonce, only when a block earned it: the CPU
|
|
97
95
|
// spans are the first paint, and where the reader's browser has WebGPU the lexer repaints them.
|
|
@@ -101,10 +99,10 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
101
99
|
+ '<html lang="en" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '">\n'
|
|
102
100
|
+ '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
|
|
103
101
|
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
104
|
-
// Chrome 154's responsively sized frames
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
102
|
+
// Chrome 154's responsively sized frames: a host frame styled `frame-sizing: content-height` sizes
|
|
103
|
+
// itself to a page that opts in with this meta, so a document exported from Rapier and carried in
|
|
104
|
+
// someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
|
|
105
|
+
// document, published by the person who exported it.
|
|
108
106
|
+ '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
|
|
109
107
|
+ '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || lexerScript || inkScript ? nonce : '', !!opts.print) + '">\n'
|
|
110
108
|
+ '<meta name="referrer" content="no-referrer">\n'
|
|
@@ -145,8 +143,9 @@ function _rapierProjectStyledRoot(semanticRoot, options) {
|
|
|
145
143
|
}
|
|
146
144
|
} else {
|
|
147
145
|
root.querySelectorAll('pre > code').forEach(code => {
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const lang = _rapierLanguageClass(code) || 'text';
|
|
147
|
+
_rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
|
|
148
|
+
_rapierArtifactMarkLexed(code, lang);
|
|
150
149
|
});
|
|
151
150
|
|
|
152
151
|
root.querySelectorAll('table').forEach(table => {
|
|
@@ -204,9 +203,8 @@ function _rapierPrefixPortableAnchors(root, baseName) {
|
|
|
204
203
|
const scopeOf = element => {
|
|
205
204
|
// querySelectorAll('*') hands back whatever the host document holds, and not every host gives
|
|
206
205
|
// each node Element.closest -- the export writer runs against parsed and stubbed documents as
|
|
207
|
-
// well as a browser's. Without this guard the whole anchor pass
|
|
208
|
-
//
|
|
209
|
-
// its references resolve document-wide, exactly as they did before drawings were scoped.
|
|
206
|
+
// well as a browser's. Without this guard the whole anchor pass would throw TypeError. No scope
|
|
207
|
+
// simply means that node's references resolve document-wide, as an unscoped drawing's do.
|
|
210
208
|
if (typeof element?.closest !== 'function') return null;
|
|
211
209
|
let svg = element.closest('svg');
|
|
212
210
|
while (typeof svg?.parentElement?.closest === 'function' && svg.parentElement.closest('svg')) svg = svg.parentElement.closest('svg');
|
|
@@ -322,12 +320,12 @@ function _rapierAnnotateExportBoxPolygons(root) {
|
|
|
322
320
|
if (!recipe) continue;
|
|
323
321
|
const view = recipe.view && recipe.view.w > 0 && recipe.view.h > 0 ? recipe.view : { x: 0, y: 0, w: recipe.canvas.w, h: recipe.canvas.h };
|
|
324
322
|
if (layout.wrap === 'around') {
|
|
325
|
-
// One occupancy for live view and export
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
323
|
+
// One occupancy for live view and export: the drawing's own profile -- closed faces reserved,
|
|
324
|
+
// open strokes as ribbons, labels as boxes -- computed from the same recipe the live view
|
|
325
|
+
// reads, sampled into bands the exported page reads back (layout/model.mjs parseProfile). Text
|
|
326
|
+
// and paint shapes need rendered pixels for their glyph silhouettes, so the live editor's own
|
|
327
|
+
// <img> supplies those when it can be found; a raster or a foreign SVG writes nothing and the
|
|
328
|
+
// export measures alpha, as the live view does.
|
|
331
329
|
try {
|
|
332
330
|
let glyphs = null;
|
|
333
331
|
if (recipe.shapes.some(shape => shape.recognized === 'text' || shape.recognized === 'paint' || shape.label)) {
|
|
@@ -405,7 +403,7 @@ async function _rapierSharedSourceCarrier(context, root) {
|
|
|
405
403
|
'"' + (forms.ids.length ? ' data-images="' + forms.ids.join(' ') + '"' : '') +
|
|
406
404
|
(forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
|
|
407
405
|
forms.carried + '\n</script>\n' +
|
|
408
|
-
// The optional edit ledger
|
|
406
|
+
// The optional edit ledger: carried only when the host binds its owner; a host without one writes none.
|
|
409
407
|
(RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
|
|
410
408
|
RapierLedgerCarried.writeBase(forms.resolved, context.proposalBase) : '');
|
|
411
409
|
}
|
|
@@ -481,7 +479,7 @@ function _rapierShareWrapKind(element) {
|
|
|
481
479
|
if (globalThis.RapierMarkdownLayout.wrapTextBlock(element)) return 'prose';
|
|
482
480
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
483
481
|
if (image && globalThis.RapierImageLayout.imageOnly(element, image)) return 'picture';
|
|
484
|
-
// An empty paragraph is transparent to the owner search
|
|
482
|
+
// An empty paragraph is transparent to the owner search, as in the editor and the export.
|
|
485
483
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
486
484
|
return null;
|
|
487
485
|
}
|
|
@@ -530,22 +528,21 @@ function _rapierSharedPageLayout(root) {
|
|
|
530
528
|
paragraph.style.setProperty('shape-outside', 'inset(' + layout.y + 'em 0 0)');
|
|
531
529
|
}
|
|
532
530
|
image.style.margin = '0';
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
// width
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
// reservation (the same graceful narrowing Share already accepts elsewhere).
|
|
531
|
+
// The reserved box comes from the same rotated-bounds geometry every other consumer uses
|
|
532
|
+
// (layout/browser.js, layout/interchange.js's `geometry.rotatedBoundsRad`, layout/model.mjs's
|
|
533
|
+
// one owner), expressed as a percentage of the column the way the unrotated branch below already
|
|
534
|
+
// is -- never the picture's own pixel dimensions, which have no relation to a requested
|
|
535
|
+
// percentage width (a 4000px-wide source at width=40% would float 1788px wide in a 600px
|
|
536
|
+
// column). Intrinsic width/height supply only the aspect ratio; a percentage padding-top/bottom
|
|
537
|
+
// resolves against the same containing-block width CSS already resolves the float's own
|
|
538
|
+
// percentage width against, so one basis serves both the box and its vertical reservation, and
|
|
539
|
+
// the box stays exactly as wide as the column at every viewport width. The turned box is
|
|
540
|
+
// centred, horizontally and vertically, in the reserved (wider, taller) box rather than
|
|
541
|
+
// left-flush inside it, so the space this paragraph clears for it (the float's own width) is
|
|
542
|
+
// evenly split around the picture on every side, not just above and below. A missing `width`
|
|
543
|
+
// falls back to the picture's own natural pixel size in pixels, the same fallback the unrotated
|
|
544
|
+
// branch below uses; without either width/height attribute the picture still turns, just without
|
|
545
|
+
// the extra reservation (the same graceful narrowing Share already accepts elsewhere).
|
|
549
546
|
const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
|
|
550
547
|
if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
|
|
551
548
|
const rad = layout.rotate * Math.PI / 180, aspect = naturalHeight / naturalWidth;
|
|
@@ -589,16 +586,15 @@ function _rapierSharedPageLayout(root) {
|
|
|
589
586
|
if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
|
|
590
587
|
}
|
|
591
588
|
if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
// that inset instead of silhouette wrapping -- a documented, narrow degradation.
|
|
589
|
+
// `wrap=around` wraps to the picture's own alpha silhouette the way standard-adoption.md's
|
|
590
|
+
// rung-2 paragraph promises -- `shape-outside:url(<the same picture>)`, which every engine that
|
|
591
|
+
// reads shape-outside re-samples from the referenced image's own pixels, no script required. A
|
|
592
|
+
// CSS transform does not turn the shape a `shape-outside` url() samples (it stays the picture's
|
|
593
|
+
// unturned alpha), so a rotated `around` picture keeps the plain rectangular reservation above
|
|
594
|
+
// instead -- an honest conservative box rather than a silhouette shaped for the wrong angle.
|
|
595
|
+
// `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
|
|
596
|
+
// combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
|
|
597
|
+
// keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
|
|
602
598
|
if (layout.wrap === 'around' && !layout.rotate && !layout.y) {
|
|
603
599
|
const src = image.getAttribute('src');
|
|
604
600
|
if (src) {
|
|
@@ -610,12 +606,12 @@ function _rapierSharedPageLayout(root) {
|
|
|
610
606
|
paragraph.classList.add('rapier-wrap-picture');
|
|
611
607
|
owner.before(paragraph);
|
|
612
608
|
}
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
609
|
+
// `behind`/`front` position the picture exactly where `x`/`y`/`width` say, out of the words' flow
|
|
610
|
+
// entirely (z-index below or above them) -- the absolutely positioned picture
|
|
611
|
+
// standard-adoption.md's rung-2 paragraph promises for these two placements, no obstacle
|
|
612
|
+
// avoidance, no script. The picture's own paragraph collapses to zero height in place (the same
|
|
613
|
+
// collapse-in-place anchor layout/interchange.js's own reflow uses for every positioned kind) so
|
|
614
|
+
// removing the picture from flow costs the surrounding text nothing.
|
|
619
615
|
const positionedAssignments = [];
|
|
620
616
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
621
617
|
const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
|
|
@@ -644,9 +640,9 @@ function _rapierSharedPageLayout(root) {
|
|
|
644
640
|
if (layout.wrap === 'behind') image.style.zIndex = '-1';
|
|
645
641
|
paragraph.classList.add('rapier-positioned-picture');
|
|
646
642
|
}
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
643
|
+
// A turned picture Share does not otherwise position (inline, which this static page does not
|
|
644
|
+
// float at all) still turns -- the same picture, painted in place, never upright just because
|
|
645
|
+
// Share's own float model has nothing else to say about it.
|
|
650
646
|
const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
|
|
651
647
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
652
648
|
if (positioned.has(image)) continue;
|
|
@@ -723,8 +719,8 @@ async function _rapierBuildSharedPage(captured) {
|
|
|
723
719
|
_rapierShareCapLongCodeBlocks(root);
|
|
724
720
|
},
|
|
725
721
|
}, context);
|
|
726
|
-
// No cap and no warning
|
|
727
|
-
//
|
|
722
|
+
// No cap and no warning: the page is as large as the document is. A page over the editor's own 25
|
|
723
|
+
// MiB open limit still opens in any browser.
|
|
728
724
|
const blob = new Blob([artifact.html], {type: 'text/html;charset=utf-8'});
|
|
729
725
|
return {blob, filename: context.baseName + '.html'};
|
|
730
726
|
}
|
|
@@ -1171,7 +1167,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1171
1167
|
return geometry.polygonProfile(corners);
|
|
1172
1168
|
}
|
|
1173
1169
|
|
|
1174
|
-
//
|
|
1170
|
+
// As the live editor (browser.js).
|
|
1175
1171
|
function syncInlineRotatedPictures(exclude) {
|
|
1176
1172
|
for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
|
|
1177
1173
|
if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
|
|
@@ -1208,7 +1204,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1208
1204
|
if (metadata.wrapTextBlock(element)) return 'prose';
|
|
1209
1205
|
const image = element.tagName === 'P' ? element.querySelector('img') : null;
|
|
1210
1206
|
if (image && imageOnly(element, image)) return 'picture';
|
|
1211
|
-
// An empty paragraph is transparent to the owner search, as in the editor
|
|
1207
|
+
// An empty paragraph is transparent to the owner search, as in the editor.
|
|
1212
1208
|
if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
|
|
1213
1209
|
return null;
|
|
1214
1210
|
}
|
|
@@ -1326,13 +1322,15 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1326
1322
|
const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
|
|
1327
1323
|
const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
|
|
1328
1324
|
let y = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
|
|
1325
|
+
// The standoff and the room between pictures, in the reference scale's pixels, as the live editor.
|
|
1326
|
+
const scale = geometry.frameScale(image), room = 10 * scale;
|
|
1329
1327
|
if (!outOfFlow(layout)) {
|
|
1330
1328
|
for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
|
|
1331
|
-
if (obstacle.x < left + rectangle.width +
|
|
1332
|
-
obstacle.y + obstacle.height > y -
|
|
1333
|
-
y = obstacle.y + obstacle.height +
|
|
1329
|
+
if (obstacle.x < left + rectangle.width + room && obstacle.x + obstacle.width > left - room &&
|
|
1330
|
+
obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
|
|
1331
|
+
y = obstacle.y + obstacle.height + room;
|
|
1334
1332
|
}
|
|
1335
|
-
//
|
|
1333
|
+
// A picture pushed down by an earlier one takes its owner with it. A deliberate `y` stays; only the collision push moves the paragraph.
|
|
1336
1334
|
const ownerTop = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
|
|
1337
1335
|
// Only the first picture of an owner takes it down.
|
|
1338
1336
|
if (!ownerLed && y > ownerTop + 0.5) {
|
|
@@ -1340,7 +1338,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1340
1338
|
bounds = box(element);
|
|
1341
1339
|
}
|
|
1342
1340
|
ownerLed = true;
|
|
1343
|
-
obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height), layout, lines, y, rectangle.height));
|
|
1341
|
+
obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale), layout, lines, y, rectangle.height));
|
|
1344
1342
|
}
|
|
1345
1343
|
placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
|
|
1346
1344
|
visualLeft: left + visualDeltaX, visualTop: y + visualDeltaY, visualWidth: fit.fit.width, visualHeight: fit.fit.height,
|
package/dist/layout/model.mjs
CHANGED
|
@@ -214,7 +214,7 @@ export function polygonProfile(corners) {
|
|
|
214
214
|
}};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
//
|
|
217
|
+
// w'=w|cos|+h|sin|, h'=w|sin|+h|cos| (markdown-standard.md). Shared by browser.js, interchange.js and share.js.
|
|
218
218
|
export function rotatedBoundsRad(width, height, rad) {
|
|
219
219
|
if (!rad) return {width, height};
|
|
220
220
|
return {width: width * Math.abs(Math.cos(rad)) + height * Math.abs(Math.sin(rad)),
|
|
@@ -255,7 +255,7 @@ export function rotatedRasterAlpha(baseAlpha, width, height, rad) {
|
|
|
255
255
|
return bandsProfile(bands);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// One occupancy descriptor
|
|
258
|
+
// One occupancy descriptor: the editor's profile sampled at 48 band centres, shipped as data-rapier-occupancy, read back here.
|
|
259
259
|
export function bandsProfile(bands) {
|
|
260
260
|
if (!Array.isArray(bands) || !bands.length) return null;
|
|
261
261
|
const at = t => bands[Math.max(0, Math.min(bands.length - 1, Math.floor(t * bands.length)))] || [];
|
|
@@ -294,10 +294,20 @@ export function parseProfile(text) {
|
|
|
294
294
|
// stroke and 10 off a level one; this stands 10 off both, within a step.
|
|
295
295
|
const shoulderSteps = 3, shoulders = Array.from({length: shoulderSteps}, (_, k) => ({from: k / shoulderSteps, to: (k + 1) / shoulderSteps, reach: Math.sqrt(1 - ((k + .5) / shoulderSteps) ** 2)}));
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// The reference scale at an element (docs/markdown-standard.md, "The reference scale"): its unit, `--md-unit` (a registered
|
|
298
|
+
// length, 16 px at M), over 16. Every length the planner adds of its own (the standoff, a band's height, the room between two
|
|
299
|
+
// pictures) is that many reference pixels times it, so a zoom moves no word against a picture.
|
|
300
|
+
export function frameScale(element) {
|
|
301
|
+
const view = element?.ownerDocument?.defaultView;
|
|
302
|
+
const unit = view ? parseFloat(view.getComputedStyle(element).getPropertyValue('--md-unit')) : NaN;
|
|
303
|
+
return unit > 0 ? unit / 16 : 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function pictureSlices(profile, x, y, width, height, scale = 1) {
|
|
307
|
+
const gap = 10 * scale;
|
|
298
308
|
if (!profile) return [{x: x - gap, y: y - gap, width: width + gap * 2, height: height + gap * 2}];
|
|
299
|
-
// 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.
|
|
300
|
-
const count = Math.max(shapeGrid, Math.min(sliceGridMax, Math.ceil((finite(height) && height > 0 ? height : 0) / slicePixelsPerBand)));
|
|
309
|
+
// A band is at most 6 reference px high at the shown size, never coarser than the descriptor's 48 nor finer than the profile's 192.
|
|
310
|
+
const count = Math.max(shapeGrid, Math.min(sliceGridMax, Math.ceil((finite(height) && height > 0 ? height : 0) / (slicePixelsPerBand * scale))));
|
|
301
311
|
let byCount = profileBands.get(profile);
|
|
302
312
|
if (!byCount) profileBands.set(profile, byCount = new Map());
|
|
303
313
|
let bands = byCount.get(count);
|
|
@@ -313,12 +323,9 @@ export function pictureSlices(profile, x, y, width, height, gap = 10) {
|
|
|
313
323
|
return span ? [span] : [];
|
|
314
324
|
}).filter(run => Number.isFinite(run[0]) && Number.isFinite(run[1]) && run[1] > run[0])
|
|
315
325
|
.sort((a, b) => a[0] - b[0]);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (last && run[0] <= last[1]) last[1] = Math.max(last[1], run[1]);
|
|
320
|
-
else merged.push([...run]);
|
|
321
|
-
}
|
|
326
|
+
// Words wrap the outer contour: a band reserves from its leftmost ink to its rightmost, so no word lands
|
|
327
|
+
// inside a hollow outline or between two strokes of one picture.
|
|
328
|
+
const merged = runs.length ? [[runs[0][0], Math.max(...runs.map(run => run[1]))]] : [];
|
|
322
329
|
const current = new Map();
|
|
323
330
|
for (const [left, right] of merged) {
|
|
324
331
|
const key = left + ':' + right;
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -369,7 +369,7 @@ export function serializeAsset(asset) {
|
|
|
369
369
|
const value = assetTitle(asset.title), title = value ? ' "' + value.replace(/&/g, '&').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"' : '';
|
|
370
370
|
return '[' + asset.label + ']: ' + asset.url + title;
|
|
371
371
|
}
|
|
372
|
-
// Pure: re-derived at commit against the moved document; appends only at the text's end
|
|
372
|
+
// Pure: re-derived at commit against the moved document; appends only at the text's end.
|
|
373
373
|
export function appendAssetText(source, asset) {
|
|
374
374
|
if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
|
|
375
375
|
const parsed = documentAssets(source), title = assetTitle(asset.title);
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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
|
-
//
|
|
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
|
|
8
|
-
//
|
|
9
|
-
// to unlock it. Omitted when off.
|
|
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 line's cap height, its foot on the
|
|
6
|
+
// Nth line's baseline, its width from its own aspect. A whole number 1 to 12; never with `width`.
|
|
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
|
|
8
|
+
// only.
|
|
9
|
+
// `lock=on`: a picture locked in place: a tap goes to the words, never the picture; a hold takes it to unlock it. Omitted when off.
|
|
10
10
|
export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'lock']);
|
|
11
11
|
export const alignments = new Set(['left', 'center', 'right', 'justify']);
|
|
12
12
|
// `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -9,16 +9,16 @@ const inkReader = () => {
|
|
|
9
9
|
const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
|
|
10
10
|
const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
|
|
11
11
|
|
|
12
|
-
// Ink (docs/markdown-standard.md, "Ink"
|
|
13
|
-
//
|
|
12
|
+
// Ink (docs/markdown-standard.md, "Ink"): a pen stroke that stays with the words it marks, a paired comment with the colour span's shape. The
|
|
13
|
+
// opener holds the stroke:
|
|
14
14
|
// <!--ink KIND [NUMBER] [COLOUR] [w=N] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
|
|
15
|
-
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when absent; box is the frame's
|
|
16
|
+
// size when the stroke was drawn and at the frame's offset from the words' box, both in hundredths of an em; w is the pen's width as Draw
|
|
17
|
+
// counts a nib (2 to 24; 9 is the default and is never written): the stroke is drawn 0.11 em wide at 9 and in proportion to w otherwise; PATH
|
|
18
|
+
// is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an em. One spelling: the fields in this order,
|
|
19
|
+
// one space between, integers only. A comment that does not read this way is ordinary comment text and marks nothing. Ink pairs do not nest
|
|
20
|
+
// in each other. Arrow/end alone take a pairing number, an end takes no other data; under, strike and arrow may omit a path for a line
|
|
21
|
+
// derived from their words.
|
|
22
22
|
const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
|
|
23
23
|
|
|
24
24
|
const INK_PATH_MAX = 160;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.73",
|
|
4
4
|
"description": "Read, render, write and style Self-contained Markdown, and carry its optional edit ledger: the shared HTML writer, semantic renderer, stylesheet, layout, marks, pictures, Will/1, replay, authorship and offline merge. MIT, no npm dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
package/style.css
CHANGED
|
@@ -3,28 +3,35 @@
|
|
|
3
3
|
* Root defaults also expose the palette to an optional page frame and the shared task glyphs
|
|
4
4
|
* to Notes. Visual rules stay inside .md-render; this sheet loads no fonts or network resources.
|
|
5
5
|
*/
|
|
6
|
+
/* The reference scale's unit (docs/markdown-standard.md, "The reference scale"): 16 px at M, a length every reader can read
|
|
7
|
+
back as pixels (a planner scales its own lengths by it). */
|
|
8
|
+
@property --md-unit{syntax:'<length>';inherits:true;initial-value:16px}
|
|
6
9
|
:root,.md-render{
|
|
10
|
+
--md-unit:1rem;
|
|
11
|
+
--md-hair:calc(var(--md-unit) / 16);
|
|
12
|
+
/* The reading measure (docs/markdown-standard.md, "The reference scale"): 36 ems of the body at M; it does not follow the step. */
|
|
13
|
+
--md-measure:39.6rem;
|
|
7
14
|
--md-font-mono:'Geist Mono',monospace;
|
|
8
15
|
--md-font-sans:'Geist',system-ui,sans-serif;
|
|
9
|
-
--md-text-sm:0.
|
|
16
|
+
--md-text-sm:calc(0.875 * var(--md-unit));
|
|
10
17
|
--md-color-text:light-dark(#121212,#fafafa);
|
|
11
18
|
--md-fw-medium:500;
|
|
12
19
|
--md-color-bg:light-dark(#ffffff,#000000);
|
|
13
|
-
--md-text-xs:0.
|
|
20
|
+
--md-text-xs:calc(0.75 * var(--md-unit));
|
|
14
21
|
--md-color-inset:light-dark(#f2f1ef,#1d1d1d);
|
|
15
22
|
--md-color-surface:light-dark(#f7f6f3,#0d0d0d);
|
|
16
|
-
--md-text-base:
|
|
23
|
+
--md-text-base:calc(1 * var(--md-unit));
|
|
17
24
|
--md-fw-semibold:600;
|
|
18
25
|
--md-fw-regular:400;
|
|
19
|
-
--md-text-body:1.
|
|
20
|
-
--md-line:1.
|
|
21
|
-
--md-space-6:
|
|
22
|
-
--md-space-4:
|
|
23
|
-
--md-space-3:
|
|
26
|
+
--md-text-body:calc(1.1 * var(--md-unit));
|
|
27
|
+
--md-line:calc(1.75 * var(--md-unit));
|
|
28
|
+
--md-space-6:calc(1.5 * var(--md-unit));
|
|
29
|
+
--md-space-4:calc(1 * var(--md-unit));
|
|
30
|
+
--md-space-3:calc(.75 * var(--md-unit));
|
|
24
31
|
--md-color-border:light-dark(#dbdad7,#1c1c1c);
|
|
25
|
-
--md-space-5:
|
|
26
|
-
--md-space-2:
|
|
27
|
-
--md-space-1:
|
|
32
|
+
--md-space-5:calc(1.25 * var(--md-unit));
|
|
33
|
+
--md-space-2:calc(.5 * var(--md-unit));
|
|
34
|
+
--md-space-1:calc(.25 * var(--md-unit));
|
|
28
35
|
--md-color-line:light-dark(#000000,#ffffff);
|
|
29
36
|
--md-color-accent-text:light-dark(color-mix(in srgb,var(--md-color-accent) 50%,#000000),color-mix(in srgb,var(--md-color-accent) 80%,#ffffff));
|
|
30
37
|
--md-track-caps:.035em;
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
--md-color-success:light-dark(#188844,#45d483);
|
|
36
43
|
--md-color-warning:light-dark(#b26105,#f5a623);
|
|
37
44
|
--md-color-error:light-dark(#c00,#ec5156);
|
|
38
|
-
--md-space-8:
|
|
45
|
+
--md-space-8:calc(2 * var(--md-unit));
|
|
39
46
|
--md-color-accent:#12A594;
|
|
40
47
|
--md-color-text-subtle:light-dark(#636363,#858585);
|
|
41
48
|
--md-heading-color:var(--md-color-text);
|
|
@@ -51,8 +58,8 @@
|
|
|
51
58
|
--md-image-width:100%;
|
|
52
59
|
--md-drawing-width:0px;
|
|
53
60
|
--md-task-box:clamp(14px,.92em,24px);
|
|
54
|
-
--md-jump-gap
|
|
55
|
-
--md-jump-marker-inline-start
|
|
61
|
+
--md-jump-gap:calc(.9 * var(--md-unit));
|
|
62
|
+
--md-jump-marker-inline-start:calc(-1.18 * var(--md-unit));
|
|
56
63
|
--md-jump-marker-center:1em;
|
|
57
64
|
--md-diagram-ink:light-dark(#121212,#fafafa);
|
|
58
65
|
--md-diagram-box:light-dark(#f0f0f0,#1d1d1d);
|
|
@@ -260,14 +267,13 @@
|
|
|
260
267
|
:where(.md-render) svg.rapier-diagram circle.state-start,:where(.md-render) svg.rapier-diagram .node .fork-join,:where(.md-render) svg.rapier-diagram .end-state-inner{fill:var(--md-color-text)!important;stroke:var(--md-color-text)!important}
|
|
261
268
|
:where(.md-render) svg.rapier-diagram circle.state-end{fill:var(--md-color-bg)!important;stroke:var(--md-color-text)!important}
|
|
262
269
|
/* The kinds that are colour by nature -- a pie's slices, a mindmap's branches, a timeline's periods,
|
|
263
|
-
a git graph's branches, a gantt chart's bars -- are drawn in the nine colours of the document
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
pink, yellow, red, green, purple, brown. */
|
|
270
|
+
a git graph's branches, a gantt chart's bars -- are drawn in the nine colours of the document
|
|
271
|
+
(rapier-notes.css, "The nine note colours"), the mate the theme gives a note card: the pale under
|
|
272
|
+
pure black ink on light paper, the deep under pure white ink on dark. Mermaid's own greys are a
|
|
273
|
+
light-paper drawing: on dark paper a mindmap and a timeline would become white cards under the
|
|
274
|
+
page's white type. The hex is copied here because a written page carries this sheet and not the
|
|
275
|
+
Notes sheet; mermaid-sanitised holds the two copies equal, pair for pair. The order keeps
|
|
276
|
+
neighbours apart: blue, orange, teal, pink, yellow, red, green, purple, brown. */
|
|
271
277
|
:where(.md-render) svg.rapier-diagram{
|
|
272
278
|
font-variant-numeric:tabular-nums;
|
|
273
279
|
}
|
|
@@ -353,12 +359,12 @@
|
|
|
353
359
|
opacity: .78;
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
/* The line model (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
/* The line model (the way a Word document reads). One line, --md-line, is the body's line box: 1.75rem, 1.6 lines
|
|
363
|
+
of the 1.1rem body, a whole 28 px at the browser's 16 px root so baselines land on pixels. Every block ends one
|
|
364
|
+
line below its last line; a heading ends half a line below. A heading's own box is the whole or half lines its
|
|
365
|
+
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. Nothing sets a top
|
|
366
|
+
margin, so a plain page (sibling margins collapsing) and the editor (a wrapper per block) space alike, and the
|
|
367
|
+
space before any block is the line its predecessor leaves. */
|
|
362
368
|
.md-render{
|
|
363
369
|
font:var(--md-fw-regular) var(--md-text-body)/var(--md-line) var(--md-font-sans);color:var(--md-color-text);text-align:start}
|
|
364
370
|
.md-render>:first-child,.md-render :is(blockquote,li,td,th,dd,.callout,details)>:first-child{margin-top:0}
|
|
@@ -423,15 +429,13 @@
|
|
|
423
429
|
vertical-align: -3px;
|
|
424
430
|
touch-action: manipulation;
|
|
425
431
|
}
|
|
426
|
-
/* The box and its tick are Feather's square and check-square
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
card's check rows read these same three (rapier-notes.css; docs/intent.md "The afternoon list", law 3:
|
|
434
|
-
one SVG per icon across Rapier). */
|
|
432
|
+
/* The box and its tick are Feather's square and check-square, with squared corners: drawn on Feather's
|
|
433
|
+
24-unit grid with its 2-unit stroke, the box's corners mitred square, and a ticked box opens at its
|
|
434
|
+
top right where the tick breaks out through it. The grid is a third wider than the box itself (the
|
|
435
|
+
box is units 3 to 21), so the painted square keeps exactly the place and size --rapier-task-box gave
|
|
436
|
+
it; the ink is --color-line through a mask, so the theme recolours it and an export needs nothing
|
|
437
|
+
more than the data: images its policy already admits. The three glyphs are written here once, and a
|
|
438
|
+
Notes card's check rows read these same three (rapier-notes.css: one SVG per icon across Rapier). */
|
|
435
439
|
:root,.md-render {
|
|
436
440
|
--md-glyph-task-box: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2'%3E%3Cpath d='M3 3h18v18H3z'/%3E%3C/svg%3E");
|
|
437
441
|
--md-glyph-task-open: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M21 12v9H3V3h13'/%3E%3C/svg%3E");
|
|
@@ -458,28 +462,28 @@
|
|
|
458
462
|
|
|
459
463
|
.md-render ol{
|
|
460
464
|
list-style:none;
|
|
461
|
-
padding-inline-start:2.
|
|
465
|
+
padding-inline-start:calc(2.75 * var(--md-unit));
|
|
462
466
|
margin:0 0 var(--md-line);
|
|
463
467
|
counter-reset:rapier-ol;
|
|
464
468
|
}
|
|
465
469
|
.md-render ol>li{
|
|
466
470
|
position:relative;
|
|
467
471
|
counter-increment:rapier-ol;
|
|
468
|
-
min-height:
|
|
469
|
-
padding-top:0.
|
|
472
|
+
min-height:calc(2 * var(--md-unit));
|
|
473
|
+
padding-top:calc(0.15 * var(--md-unit));
|
|
470
474
|
}
|
|
471
475
|
.md-render ol>li::before{
|
|
472
476
|
content:counter(rapier-ol);
|
|
473
477
|
position:absolute;
|
|
474
478
|
/* The circle is centred on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
|
|
475
|
-
inset-inline-start
|
|
476
|
-
width:
|
|
479
|
+
inset-inline-start:calc(-2.75 * var(--md-unit)); top:calc(calc(0.15 * var(--md-unit)) + (var(--md-line) - calc(2 * var(--md-unit))) / 2);
|
|
480
|
+
width:calc(2 * var(--md-unit)); height:calc(2 * var(--md-unit));
|
|
477
481
|
display:flex; align-items:center; justify-content:center;
|
|
478
482
|
border:2px solid var(--md-color-line);
|
|
479
483
|
border-radius:999px;
|
|
480
484
|
background:var(--md-color-bg);
|
|
481
485
|
color:var(--md-color-line);
|
|
482
|
-
font:var(--md-fw-medium) 0.
|
|
486
|
+
font:var(--md-fw-medium) calc(0.95 * var(--md-unit))/1 var(--md-font-sans);
|
|
483
487
|
font-variant-numeric:tabular-nums;
|
|
484
488
|
box-sizing:border-box;
|
|
485
489
|
z-index:1;
|
|
@@ -489,8 +493,8 @@
|
|
|
489
493
|
content:'';
|
|
490
494
|
position:absolute;
|
|
491
495
|
|
|
492
|
-
inset-inline-start:calc(-1.
|
|
493
|
-
top:2.
|
|
496
|
+
inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px);
|
|
497
|
+
top:calc(2.2 * var(--md-unit));
|
|
494
498
|
bottom:calc(var(--md-line) / -4);
|
|
495
499
|
border-inline-start:3px dotted var(--md-color-line);
|
|
496
500
|
z-index:0;
|
|
@@ -499,11 +503,11 @@
|
|
|
499
503
|
_rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
|
|
500
504
|
line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
|
|
501
505
|
.md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-line))}
|
|
502
|
-
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:2.
|
|
506
|
+
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
|
|
503
507
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]::before{
|
|
504
508
|
content:'';
|
|
505
509
|
position:absolute;
|
|
506
|
-
inset-inline-start:calc(
|
|
510
|
+
inset-inline-start:calc(calc(1 * var(--md-unit)) - 1.5px);
|
|
507
511
|
top:0;
|
|
508
512
|
bottom:calc(var(--md-line) / -2);
|
|
509
513
|
border-inline-start:3px dotted var(--md-color-line);
|
|
@@ -517,25 +521,25 @@
|
|
|
517
521
|
.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}
|
|
518
522
|
|
|
519
523
|
.md-render .rapier-jump-label{
|
|
520
|
-
margin:0 0 .
|
|
521
|
-
font:var(--md-fw-semibold) .
|
|
524
|
+
margin:0 0 calc(.5 * var(--md-unit));
|
|
525
|
+
font:var(--md-fw-semibold) calc(.72 * var(--md-unit))/1.4 var(--md-font-mono);
|
|
522
526
|
letter-spacing:var(--md-track-caps);
|
|
523
527
|
text-transform:uppercase;
|
|
524
528
|
color:var(--md-color-text);
|
|
525
529
|
}
|
|
526
530
|
.md-render .rapier-jump-label strong{font:inherit}
|
|
527
531
|
.md-render ul.rapier-jump-list{
|
|
528
|
-
--md-jump-gap
|
|
529
|
-
--md-jump-marker-inline-start
|
|
532
|
+
--md-jump-gap:calc(.9 * var(--md-unit));
|
|
533
|
+
--md-jump-marker-inline-start:calc(-1.18 * var(--md-unit));
|
|
530
534
|
--md-jump-marker-center:1em;
|
|
531
535
|
list-style:none;
|
|
532
|
-
padding-inline-start:1.
|
|
536
|
+
padding-inline-start:calc(1.85 * var(--md-unit));
|
|
533
537
|
margin:0 0 var(--md-line);
|
|
534
538
|
}
|
|
535
539
|
.md-render ul.rapier-jump-list>li{
|
|
536
540
|
position:relative;
|
|
537
541
|
margin:0 0 var(--md-jump-gap);
|
|
538
|
-
padding
|
|
542
|
+
padding:calc(.18 * var(--md-unit)) 0 calc(.18 * var(--md-unit)) calc(.15 * var(--md-unit));
|
|
539
543
|
}
|
|
540
544
|
.md-render ul.rapier-jump-list>li:last-child{margin-bottom:0}
|
|
541
545
|
.md-render ul.rapier-jump-list>li::before{
|
|
@@ -574,7 +578,7 @@
|
|
|
574
578
|
.md-render em{font-style:italic}
|
|
575
579
|
.md-render :not(pre)>code{line-height:1}
|
|
576
580
|
.md-render code{font-family:var(--md-font-mono);font-size:0.85em;
|
|
577
|
-
background:var(--md-color-inset);border:
|
|
581
|
+
background:var(--md-color-inset);border:var(--md-hair) solid var(--md-color-border);padding:var(--md-hair) calc(5 * var(--md-hair))}
|
|
578
582
|
.md-render pre{margin:0 0 var(--md-line);padding:var(--md-space-3) var(--md-space-4);
|
|
579
583
|
background:var(--md-color-surface-2);border:1px solid var(--md-color-border);overflow-x:auto}
|
|
580
584
|
.md-render pre code{background:none;border:none;padding:0;font-size:calc(var(--md-text-body) * 0.7386);line-height:calc(var(--md-line) * .75)}
|
|
@@ -602,18 +606,18 @@
|
|
|
602
606
|
.md-render math:not([display="block"]){font-size:1em}
|
|
603
607
|
.md-render blockquote{margin:0 0 var(--md-line);
|
|
604
608
|
padding-block:var(--md-space-1);padding-inline-start:var(--md-space-4);padding-inline-end:0;
|
|
605
|
-
border-inline-start:
|
|
609
|
+
border-inline-start:calc(2 * var(--md-hair)) solid var(--md-color-border-focus);color:var(--md-color-text-secondary)}
|
|
606
610
|
.md-render blockquote blockquote{
|
|
607
611
|
margin:calc(var(--md-line) / 2) 0 calc(var(--md-line) / 4);padding-inline-start:var(--md-space-3);
|
|
608
612
|
border-inline-start-color:color-mix(in srgb,var(--md-color-border-focus) 70%,var(--md-color-text-muted));
|
|
609
613
|
color:color-mix(in srgb,var(--md-color-text-secondary) 84%,var(--md-color-text-muted))
|
|
610
614
|
}
|
|
611
615
|
.md-render blockquote blockquote blockquote{
|
|
612
|
-
border-inline-start-width:
|
|
616
|
+
border-inline-start-width:var(--md-hair);color:var(--md-color-text-muted)
|
|
613
617
|
}
|
|
614
618
|
.md-render blockquote.callout{
|
|
615
619
|
--md-callout-color:var(--md-callout-note);
|
|
616
|
-
padding:var(--md-space-2) var(--md-space-4);border:0;border-inline-start:
|
|
620
|
+
padding:var(--md-space-2) var(--md-space-4);border:0;border-inline-start:calc(4 * var(--md-hair)) solid var(--md-callout-color);
|
|
617
621
|
background:none;color:var(--md-color-text);border-radius:0
|
|
618
622
|
}
|
|
619
623
|
.md-render blockquote.callout-tip{--md-callout-color:var(--md-color-success)}
|
|
@@ -629,7 +633,7 @@
|
|
|
629
633
|
.md-render blockquote.callout>:last-child{margin-bottom:0}
|
|
630
634
|
@media print{.md-render{--md-callout-note:#0969da;--md-callout-important:#8250df}}
|
|
631
635
|
|
|
632
|
-
.md-render details { border:
|
|
636
|
+
.md-render details { border: var(--md-hair) solid var(--md-color-border); border-radius: 0; padding: var(--md-space-2) var(--md-space-4); margin: 0 0 var(--md-line); }
|
|
633
637
|
.md-render details[open] { padding-bottom: var(--md-space-3); }
|
|
634
638
|
/* A section with nothing typed into it yet has no paragraph to tap: without this, the only body
|
|
635
639
|
space below the summary is the plain padding-bottom above (12px), too thin a margin for a real
|
|
@@ -662,7 +666,7 @@
|
|
|
662
666
|
position:absolute;
|
|
663
667
|
left:50%;
|
|
664
668
|
top:50%;
|
|
665
|
-
width:7.
|
|
669
|
+
width:calc(7.5 * var(--md-unit));
|
|
666
670
|
height:2px;
|
|
667
671
|
transform:translate(-50%,-50%);
|
|
668
672
|
background:linear-gradient(to right,
|
|
@@ -705,10 +709,10 @@
|
|
|
705
709
|
border-collapse:collapse;
|
|
706
710
|
margin:0 0 var(--md-line);
|
|
707
711
|
font-size:var(--md-text-sm);
|
|
708
|
-
border:
|
|
712
|
+
border:calc(2 * var(--md-hair)) solid var(--md-color-line);
|
|
709
713
|
}
|
|
710
714
|
.md-render th,.md-render td{
|
|
711
|
-
border:
|
|
715
|
+
border:var(--md-hair) solid color-mix(in srgb, var(--md-color-line) 74%, transparent);
|
|
712
716
|
padding:var(--md-space-2) var(--md-space-3);
|
|
713
717
|
text-align:left;
|
|
714
718
|
}
|
|
@@ -825,7 +829,7 @@
|
|
|
825
829
|
this is the sheet every written page carries -- G3 reached the same conclusion independently, and
|
|
826
830
|
the content sheet is where a look belongs. */
|
|
827
831
|
.md-render pre[data-rapier-code-lines]{position:relative;max-height:33em;overflow:auto;padding-top:28px}
|
|
828
|
-
.md-render .rapier-code-lines-note{position:absolute;top:6px;right:10px;font
|
|
832
|
+
.md-render .rapier-code-lines-note{position:absolute;top:6px;right:10px;font:calc(.7 * var(--md-unit))/1 var(--md-font-sans);color:var(--md-color-text-muted);background:var(--md-color-inset);padding:.15em .5em;pointer-events:none}
|
|
829
833
|
@media print{
|
|
830
834
|
.md-render pre[data-rapier-code-lines]{max-height:none!important;overflow:visible!important;padding-top:12px!important}
|
|
831
835
|
.md-render .rapier-code-lines-note{display:none!important}
|