rapier-markdown-kit 1.2.5 → 1.2.7
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/LICENSE +3 -3
- package/README.md +12 -12
- package/dist/kit/pdf-glyphs.mjs +44 -6
- package/dist/kit/render-markdown.mjs +2 -2
- package/dist/kit/render-print.mjs +7 -3
- package/dist/kit/render-sanitize.mjs +7 -5
- package/dist/kit/render-styles.mjs +2 -2
- package/dist/kit/render-work.mjs +1 -1
- package/dist/kit/render.mjs +8 -6
- package/dist/packages/rapier-diff/concerns.mjs +1 -1
- package/dist/packages/rapier-diff/walk.mjs +1 -1
- package/dist/spec/document-settings.mjs +92 -6
- package/dist/spec/md-assets.mjs +27 -6
- package/dist/spec/md-layout.mjs +3 -3
- package/dist/spec/md-marks.mjs +4 -4
- package/dist/spec/md-source.mjs +8 -4
- package/dist/spec/word-sidecar.mjs +7 -0
- package/package.json +1 -1
- package/style.css +15 -18
package/LICENSE
CHANGED
|
@@ -20,7 +20,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
22
|
|
|
23
|
-
Files under this
|
|
23
|
+
Files under this license carry `SPDX-License-Identifier: MIT` in their first lines: the standard's
|
|
24
24
|
grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/md-source.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`), ink geometry
|
|
25
25
|
(`spec/ink.mjs`),
|
|
26
26
|
the layout model and line planner (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.mjs`,
|
|
@@ -42,8 +42,8 @@ AGPL-3.0-only. The page wrapper (`skills/rapier-html/wrap.mjs`, `skills/rapier-h
|
|
|
42
42
|
`kit/pdf-vectors.mjs`, `kit/pdf-glyphs.mjs`, `kit/stage.mjs` and MIT-marked conformance
|
|
43
43
|
modules are covered too. These names describe the Rapier-owned files carrying the
|
|
44
44
|
MIT notice, not a grant over the editor or third-party code. Other Rapier-owned
|
|
45
|
-
files retain their stated
|
|
46
|
-
Third-party components retain their own copyright and
|
|
45
|
+
files retain their stated license; the editor is AGPL-3.0-only (see `LICENSE`).
|
|
46
|
+
Third-party components retain their own copyright and license notices.
|
|
47
47
|
|
|
48
48
|
The portable font helpers read document definitions and accept explicit host validation and
|
|
49
49
|
subsetting functions. The application binding (`editor/export-fonts.mjs`), captured-font
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Read, render and write Self-contained Markdown in browsers and Node: one `.md` f
|
|
|
4
4
|
drawings, layout and precise inline typography. Build portable documents for people and agents. Will/1 regions mark what an agent
|
|
5
5
|
may edit, append to or preserve. MIT, no dependencies.
|
|
6
6
|
|
|
7
|
-
- Typography uses ordinary inline HTML spans with validated size,
|
|
7
|
+
- Typography uses ordinary inline HTML spans with validated size, color and family. Comments and references
|
|
8
8
|
carry other optional facts; each reader chooses its HTML, style, extension and resource support.
|
|
9
9
|
- One stylesheet renders the document as the editor and its exported page do.
|
|
10
10
|
- The line planner flows text around a picture's real shape; a conformance suite of thirteen documents runs
|
|
@@ -93,7 +93,7 @@ DOM objects between document realms.
|
|
|
93
93
|
|
|
94
94
|
This **DOM-hosted library** requires a DOM and sanitizer, the Rapier grammar,
|
|
95
95
|
portable image preparation, style access and the named codec/layout ports it uses.
|
|
96
|
-
Those dependencies keep their own
|
|
96
|
+
Those dependencies keep their own licenses: the MIT grant here does not relicense a
|
|
97
97
|
separately supplied host or vendor. The [editor host binding](https://github.com/jackskip22/rapier/blob/main/src/editor/render-host.js)
|
|
98
98
|
lists the renderer's dependencies. [rapier-server](https://github.com/jackskip22/rapier/blob/main/server/README.md)
|
|
99
99
|
provides a complete local host with a pinned Chromium process. Its Word exporter and filesystem service are AGPL,
|
|
@@ -118,7 +118,7 @@ Browser hosts additionally check the exact captured container with an uninstalle
|
|
|
118
118
|
Without `FontFace`, only the host validator's admission is established; it does not establish
|
|
119
119
|
browser container support. The HTML font helpers do not bundle the application's font importer,
|
|
120
120
|
container codecs or subsetter. Rapier binds its existing materializer and shared importer at `editor/render-host.js` through
|
|
121
|
-
`editor/export-fonts.mjs`; those implementations retain their application
|
|
121
|
+
`editor/export-fonts.mjs`; those implementations retain their application license.
|
|
122
122
|
An embedded family named Geist or Geist Mono remains custom data and takes precedence over the
|
|
123
123
|
factory family. Custom fonts have no optional factory size limit.
|
|
124
124
|
|
|
@@ -140,7 +140,7 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
140
140
|
| Import | What it is |
|
|
141
141
|
| --- | --- |
|
|
142
142
|
| `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
|
|
143
|
-
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved
|
|
143
|
+
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
|
|
144
144
|
| `rapier-markdown-kit/model` | Occupancy profiles and the line planner: the wrapping itself. |
|
|
145
145
|
| `rapier-markdown-kit/typography` | Source-preserving paragraph refinement through `createTypography()`. |
|
|
146
146
|
| `rapier-markdown-kit/will` | The Will/1 grammar (a separate, optional standard): the markers around an agent's intent regions. |
|
|
@@ -244,11 +244,11 @@ font-data authority, including a name that collides with a factory face.
|
|
|
244
244
|
inline span; split block selections and keep their `#`, list markers and separators outside. `typography.parseStyle`
|
|
245
245
|
returns only admitted facts. `typography.install(md)` installs that interpretation on the caller's markdown-it
|
|
246
246
|
parser, and `typographySourceRanges(source, md)` reports original UTF-16 wrapper ranges through that parser.
|
|
247
|
-
These calls neither mutate the source nor load resources. The installer also reads already-authored
|
|
247
|
+
These calls neither mutate the source nor load resources. The installer also reads already-authored color and
|
|
248
248
|
font comments into the same facts; an ordinary open/save must preserve their bytes, not rewrite them as spans.
|
|
249
249
|
|
|
250
250
|
Sizes accept positive decimal `pt`/`px`, at most 12 fractional digits, up to 1638pt/2184px; 96px = 72pt. New values
|
|
251
|
-
use lowercase units and omit insignificant zeros.
|
|
251
|
+
use lowercase units and omit insignificant zeros. Colors become lowercase six-digit RGB hex; opaque integer
|
|
252
252
|
RGB CSSOM values are also read. Families are one validated name, an unquoted CSS generic, or an existing preset
|
|
253
253
|
stack. `sans`, `serif`, `mono`, `system`, `hand` are reserved choice aliases, not quoted custom identities. Styles
|
|
254
254
|
are bounded to 4096 characters, 32 declarations and 64 paired levels. Invalid declarations and unknown properties
|
|
@@ -271,7 +271,7 @@ unit, independent of editor zoom. No mapping claims identical glyphs or line bre
|
|
|
271
271
|
See the original [typography guidance](https://github.com/jackskip22/rapier/blob/main/docs/markdown-standard.md#inline-typography),
|
|
272
272
|
[CommonMark raw HTML](https://spec.commonmark.org/0.31.2/#raw-html), [CSS absolute units](https://www.w3.org/TR/css-values-4/#absolute-lengths),
|
|
273
273
|
[Pandoc spans](https://pandoc.org/MANUAL.html#extension-bracketed_spans) and [Quarto spans](https://quarto.org/docs/authoring/markdown-basics.html#divs-and-spans).
|
|
274
|
-
Those specifications retain their own
|
|
274
|
+
Those specifications retain their own licenses.
|
|
275
275
|
|
|
276
276
|
## Render with the reference style
|
|
277
277
|
|
|
@@ -351,9 +351,9 @@ export const exampleSource = [
|
|
|
351
351
|
'plain code',
|
|
352
352
|
'~~~',
|
|
353
353
|
'',
|
|
354
|
-
'A
|
|
354
|
+
'A centered paragraph.' + formatLayout({align: 'center'}),
|
|
355
355
|
'',
|
|
356
|
-
'A ' + formatColorRun(TEXT_COLOR_NAMES.blue, 'blue phrase') + ' keeps its
|
|
356
|
+
'A ' + formatColorRun(TEXT_COLOR_NAMES.blue, 'blue phrase') + ' keeps its color.',
|
|
357
357
|
'',
|
|
358
358
|
'And ' + typography.wrap({size: '13.5pt', color: '#175dc4', family: 'serif'}, '**precise words**') + ' keep their emphasis.',
|
|
359
359
|
'',
|
|
@@ -499,11 +499,11 @@ Its historical line counts are not a current-engine or font-shaping acceptance g
|
|
|
499
499
|
`--impl path/to/module.mjs` runs another implementation against the same documents; `--tolerance 2` sets the pixel
|
|
500
500
|
tolerance.
|
|
501
501
|
|
|
502
|
-
##
|
|
502
|
+
## License
|
|
503
503
|
|
|
504
|
-
MIT, the full text in `LICENSE`; Rapier-owned modules and the stylesheet keep their MIT line. Pretext's complete
|
|
504
|
+
MIT, the full text in `LICENSE`; Rapier-owned modules and the stylesheet keep their MIT line. Pretext's complete license,
|
|
505
505
|
notice and pinned source inventory are in `dist/agent/vendor/pretext/`. The selected upstream is unreleased main
|
|
506
|
-
`bac1774c0f8b7a92e44b9da43b6d5443d55d0d9b`, despite its package version `0.0.9`. Its upstream
|
|
506
|
+
`bac1774c0f8b7a92e44b9da43b6d5443d55d0d9b`, despite its package version `0.0.9`. Its upstream license includes
|
|
507
507
|
Chromium/WebKit BSD, Unicode and ICU notices for derived code and generated data; those notices travel with the kit
|
|
508
508
|
and the exported wrapping runtime. The history projection uses the MIT diff core;
|
|
509
509
|
its full grant is in `dist/packages/rapier-diff/LICENSE`. The Rapier editor is AGPL-3.0-only and
|
package/dist/kit/pdf-glyphs.mjs
CHANGED
|
@@ -77,6 +77,14 @@ function shape(value, font, preserve, ligatures) {
|
|
|
77
77
|
const result = [];
|
|
78
78
|
for (let at = 0; at < value.length;) {
|
|
79
79
|
const code = value.codePointAt(at), character = String.fromCodePoint(code), start = at;
|
|
80
|
+
if (code === 0x00ad) {
|
|
81
|
+
at += character.length;
|
|
82
|
+
const own = font.glyph(code), hyphen = font.glyph(0x2d), glyph = own || hyphen;
|
|
83
|
+
// A discretionary hyphen with no glyph stays in the page paint. Do not skip the lookup.
|
|
84
|
+
if (!glyph || glyph >= font.glyphCount) return null;
|
|
85
|
+
result.push({start, end: at, text: own ? character : '-', glyph, soft: true});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
80
88
|
if (/[\p{Mark}\p{Format}]/u.test(character) || (/[\p{Letter}]/u.test(character) && !/\p{Script=Latin}/u.test(character)) || code === 0xfb01 || code === 0xfb02) return null;
|
|
81
89
|
at += character.length;
|
|
82
90
|
if (!preserve && /[ \t\r\n\f]/.test(character)) {while (at < value.length && /[ \t\r\n\f]/.test(value[at])) at++;}
|
|
@@ -97,7 +105,13 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
97
105
|
const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
|
|
98
106
|
const nodes = new Map(), hidden = [], canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
|
|
99
107
|
const context = canvas.getContext('2d', {willReadFrequently: true});
|
|
100
|
-
const restore = () => {
|
|
108
|
+
const restore = () => {
|
|
109
|
+
for (const row of hidden) {
|
|
110
|
+
if (row.wrap) {row.wrap.replaceWith(...row.wrap.childNodes); continue;}
|
|
111
|
+
if (row.value) row.node.style.setProperty('-webkit-text-fill-color', row.value, row.priority); else row.node.style.removeProperty('-webkit-text-fill-color');
|
|
112
|
+
}
|
|
113
|
+
canvas.width = canvas.height = 0;
|
|
114
|
+
};
|
|
101
115
|
if (!context) return {nodes, restore};
|
|
102
116
|
const colorCache = new Map(), styles = new Map(), styleOf = node => {if (!styles.has(node)) styles.set(node, window.getComputedStyle(node)); return styles.get(node);};
|
|
103
117
|
const color = value => {if (!colorCache.has(value)) {context.clearRect(0, 0, 1, 1); context.fillStyle = value; context.fillRect(0, 0, 1, 1); colorCache.set(value, Array.from(context.getImageData(0, 0, 1, 1).data, item => item / 255));} return colorCache.get(value);};
|
|
@@ -118,7 +132,7 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
118
132
|
if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
|
|
119
133
|
// Leaving the existing element in place preserves selectors, counters, line breaking and inline marks.
|
|
120
134
|
// Keep preformatted blocks, their inline struts and syntax paint together.
|
|
121
|
-
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.children.length || element.closest('svg,pre,.rapier-will-carrier')
|
|
135
|
+
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.children.length || element.closest('svg,pre,.rapier-will-carrier')) continue;
|
|
122
136
|
const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3);
|
|
123
137
|
if (textNodes.length !== 1 || !textNodes[0].data.trim() || !safe(element)) continue;
|
|
124
138
|
const language = (element.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || '').toLowerCase();
|
|
@@ -142,15 +156,39 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
142
156
|
if (!Number.isFinite(ascent)) continue;
|
|
143
157
|
const rgba = color(style.webkitTextFillColor || style.color), range = document.createRange();
|
|
144
158
|
let valid = true;
|
|
159
|
+
const placed = [];
|
|
145
160
|
for (const item of shaped) {
|
|
146
161
|
range.setStart(node, item.start); range.setEnd(node, item.end);
|
|
147
|
-
const
|
|
148
|
-
|
|
162
|
+
const ink = Array.from(range.getClientRects()).filter(rect => rect.height > 0);
|
|
163
|
+
// A break after a soft hyphen can leave the next letter with a phantom box on the
|
|
164
|
+
// hyphen's line. Keep the line that actually holds the ink.
|
|
165
|
+
const lines = [];
|
|
166
|
+
for (const rect of ink) {
|
|
167
|
+
const line = lines.find(row => Math.abs(row[0].top - rect.top) <= .25);
|
|
168
|
+
if (line) line.push(rect); else lines.push([rect]);
|
|
169
|
+
}
|
|
170
|
+
lines.sort((a, b) => b.reduce((sum, rect) => sum + rect.width, 0) - a.reduce((sum, rect) => sum + rect.width, 0));
|
|
171
|
+
const previous = shaped[shaped.indexOf(item) - 1];
|
|
172
|
+
if (lines.length > 1 && !item.soft && !previous?.soft) {valid = false; break;}
|
|
173
|
+
const rects = lines[0] || [];
|
|
174
|
+
const first = (item.soft ? rects.find(rect => rect.width > 0) : null) || rects[0];
|
|
175
|
+
// An unbroken soft hyphen has no ink. Leave it out of the vector run.
|
|
176
|
+
if (item.soft && !(first?.width > 0)) continue;
|
|
177
|
+
if (!first) {valid = false; break;}
|
|
149
178
|
const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
|
|
150
179
|
Object.assign(item, {font, size, rgba, x: first.left - origin.left - page * width, baseline: first.top - origin.top + ascent});
|
|
180
|
+
placed.push(item);
|
|
181
|
+
}
|
|
182
|
+
if (!valid || !placed.length) continue;
|
|
183
|
+
nodes.set(node, placed);
|
|
184
|
+
if (element.localName === 'li') {
|
|
185
|
+
// A list item's marker inherits its fill and is not vectorized: hide only the words.
|
|
186
|
+
const wrap = document.createElement('span');
|
|
187
|
+
node.replaceWith(wrap); wrap.append(node);
|
|
188
|
+
wrap.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
|
|
189
|
+
hidden.push({wrap});
|
|
190
|
+
continue;
|
|
151
191
|
}
|
|
152
|
-
if (!valid) continue;
|
|
153
|
-
nodes.set(node, shaped);
|
|
154
192
|
hidden.push({node: element, value: element.style.getPropertyValue('-webkit-text-fill-color'), priority: element.style.getPropertyPriority('-webkit-text-fill-color')});
|
|
155
193
|
element.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
|
|
156
194
|
}
|
|
@@ -325,7 +325,7 @@ function initMarkdownIt() {
|
|
|
325
325
|
const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
|
|
326
326
|
const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
|
|
327
327
|
const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
|
|
328
|
-
if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
|
|
328
|
+
if (!ready && !provider?.request?.() && env.rapierProviders !== false && !_diagramPromptedOnce) {
|
|
329
329
|
_diagramPromptedOnce = true;
|
|
330
330
|
try { _rapierUiDiagram.request(); } catch (_) {}
|
|
331
331
|
}
|
|
@@ -366,7 +366,7 @@ function initMarkdownIt() {
|
|
|
366
366
|
if (env.rapierPage) return '<span data-rapier-math-source="">' + literal + '</span>';
|
|
367
367
|
const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
|
|
368
368
|
if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
|
|
369
|
-
if (env.rapierProviders !== false && !_mathPromptedOnce) {
|
|
369
|
+
if (!mathProvider?.request?.() && env.rapierProviders !== false && !_mathPromptedOnce) {
|
|
370
370
|
_mathPromptedOnce = true;
|
|
371
371
|
try { _rapierUiMath.request(); } catch (_) {}
|
|
372
372
|
}
|
|
@@ -42,8 +42,12 @@ function _rapierPrintWillAdmit(canonical, plan) {
|
|
|
42
42
|
if (new Set(plan.markers.flatMap(marker => Array.from(marker))).size > 65000) throw _rapierPrintWillLost('the markers use more distinct characters than a font can hold', 'Shorten them');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function _rapierPrintWillFont(markers) {
|
|
46
|
-
const
|
|
45
|
+
function _rapierPrintWillFont(markers, advanceOverride) {
|
|
46
|
+
const metrics = _rapierPrintWillMetrics(markers);
|
|
47
|
+
const size = metrics.size;
|
|
48
|
+
// The page's carrier face keeps the short marker advance. Searchable text is
|
|
49
|
+
// positioned at 500 units, which is also /DW, so this copy can be asked to match it.
|
|
50
|
+
const advance = advanceOverride ?? metrics.advance;
|
|
47
51
|
const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
|
|
48
52
|
const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
|
|
49
53
|
.concat(scalars.filter(code => code >= 0x10000).sort((a, b) => b - a));
|
|
@@ -296,7 +300,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
296
300
|
glyphs.add(character);
|
|
297
301
|
if (glyphs.size > 65000) refuse('pdf_text_glyph_limit');
|
|
298
302
|
}
|
|
299
|
-
const font = _rapierPrintWillFont(text);
|
|
303
|
+
const font = _rapierPrintWillFont(text, 500);
|
|
300
304
|
strip.style.width = count * bodyWidth + 'px';
|
|
301
305
|
strip.style.transform = 'none';
|
|
302
306
|
// One styled film is reused for every page window. Batches stay inside the capture edge
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {typography} from '../spec/md-marks.mjs';
|
|
3
3
|
const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
|
|
4
|
-
|
|
4
|
+
// Invokers (DOMPurify 3.4.16 allows them) act on another element by id, and a document shares the page with the editor's own
|
|
5
|
+
// controls: a document's button could open or close them.
|
|
6
|
+
const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for', 'command', 'commandfor', 'popover', 'popovertarget', 'popovertargetaction']);
|
|
5
7
|
// The document profiles use one allowlist in a browser and in an inert server parser.
|
|
6
8
|
const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
7
9
|
htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
|
|
8
10
|
svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
|
|
9
11
|
mathTags: Object.freeze(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),
|
|
10
|
-
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
|
|
12
|
+
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
|
|
11
13
|
svgAttributes: Object.freeze(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","pointer-events","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","vector-effect","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),
|
|
12
14
|
mathAttributes: Object.freeze(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),
|
|
13
15
|
xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
|
|
@@ -270,7 +272,7 @@ function _rapierInstallSanitizeHooks() {
|
|
|
270
272
|
if (!/^data:/i.test(compact)) return;
|
|
271
273
|
const tag = node?.nodeName === 'IMG' ? 'img'
|
|
272
274
|
: node?.namespaceURI === DIAGRAM_LABEL_SVG_NAMESPACE && node.localName === 'image' ? 'image' : '';
|
|
273
|
-
const rasterAllowed = (/^(?:render|export|raw|source)$/.test(_rapierSanitizeRuntime.context)
|
|
275
|
+
const rasterAllowed = (/^(?:paste|render|export|raw|source)$/.test(_rapierSanitizeRuntime.context)
|
|
274
276
|
|| _rapierSanitizeRuntime.context === 'diagram' && tag === 'image')
|
|
275
277
|
&& _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
|
|
276
278
|
if (!rasterAllowed) data.keepAttr = false;
|
|
@@ -309,14 +311,14 @@ function _rapierInstallSanitizeHooks() {
|
|
|
309
311
|
// know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
|
|
310
312
|
// for the conversion that reads them (_rapierPasteWordLists).
|
|
311
313
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
312
|
-
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|SPAN)$/.test(node.nodeName)) return;
|
|
314
|
+
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
|
|
313
315
|
// Only the style names them: a page cannot bring these attributes of its own.
|
|
314
316
|
node.removeAttribute('data-rapier-word-level');
|
|
315
317
|
node.removeAttribute('data-rapier-word-marker');
|
|
316
318
|
const style = node.getAttribute('style');
|
|
317
319
|
if (!style || !/mso-list/i.test(style)) return;
|
|
318
320
|
const {wordListLevel, wordListIsMarker} = globalThis.RapierMarkdownSpec;
|
|
319
|
-
const level = node.nodeName
|
|
321
|
+
const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
|
|
320
322
|
if (level) node.setAttribute('data-rapier-word-level', String(level));
|
|
321
323
|
else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
|
|
322
324
|
});
|
|
@@ -54,7 +54,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
54
54
|
@page{size:auto;margin:12mm 14mm 14mm}
|
|
55
55
|
@media print{
|
|
56
56
|
${p('.md-render')}{--md-color-line:#000000!important}
|
|
57
|
-
${list('.md-render h1','.md-render h2','.md-render h3','.md-render h4','.md-render h5','.md-render h6')}{break-after:avoid
|
|
57
|
+
${list('.md-render h1','.md-render h2','.md-render h3','.md-render h4','.md-render h5','.md-render h6')}{page-break-after:avoid;break-after:avoid-page;color:#111}
|
|
58
58
|
${list('.md-render p','.md-render li','.md-render blockquote')}{orphans:3;widows:3}
|
|
59
59
|
${p('.md-render img')}{max-width:100%!important;height:auto!important;break-inside:avoid-page}
|
|
60
60
|
${list('.md-render pre','.artifact-flat')}{white-space:pre-wrap!important;overflow-wrap:anywhere!important;word-break:break-word!important;overflow:visible!important;max-height:none!important}
|
|
@@ -66,7 +66,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
66
66
|
${list('.md-render th','.md-render td')}{overflow-wrap:anywhere;word-break:normal}
|
|
67
67
|
${p('.md-render thead')}{display:table-header-group}
|
|
68
68
|
${p('.md-render tfoot')}{display:table-footer-group}
|
|
69
|
-
${p('.md-render tr')}{break-inside:avoid
|
|
69
|
+
${p('.md-render tr')}{page-break-inside:avoid;break-inside:avoid-page}
|
|
70
70
|
${p('.md-render details>*:not(summary)')}{display:block!important}
|
|
71
71
|
${p('.md-render details')}{break-inside:auto}
|
|
72
72
|
${p('.md-render a')}{color:#111!important;text-decoration:underline}
|
package/dist/kit/render-work.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export function finish(iterator) {
|
|
|
8
8
|
|
|
9
9
|
export function check(work) {
|
|
10
10
|
work?.check?.();
|
|
11
|
-
if (work?.signal?.aborted) throw Object.assign(new Error('The operation was
|
|
11
|
+
if (work?.signal?.aborted) throw Object.assign(new Error('The operation was canceled.'), {name: 'AbortError', code: 'cancelled'});
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function pause(work, fraction, label) {
|
package/dist/kit/render.mjs
CHANGED
|
@@ -161,7 +161,9 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
161
161
|
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
162
162
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
163
163
|
let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || completePage, false, opts.print && opts.kind === 'standalone' ? '' : null)
|
|
164
|
-
|
|
164
|
+
// Print already floats pictures. Tables, code, figures and rules clear them
|
|
165
|
+
// through the same clearing rule a complete page uses; print keeps its own page flow.
|
|
166
|
+
+ (completePage || opts.print ? '\n\n' + _rapierSharedPageFallbackCss(!completePage) : '')
|
|
165
167
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
166
168
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
167
169
|
+ (settingsCss ? '\n' + settingsCss : '')
|
|
@@ -340,7 +342,7 @@ function _rapierDocxRenderStyles(root) {
|
|
|
340
342
|
const surface = into.createElement('div'); surface.className = 'md-render'; into.body.appendChild(surface);
|
|
341
343
|
const canvas = into.createElement('canvas'); canvas.width = canvas.height = 1;
|
|
342
344
|
const paint = canvas.getContext('2d'), colours = new Map();
|
|
343
|
-
if (!paint) throw new Error('Word text
|
|
345
|
+
if (!paint) throw new Error('Word text colors could not be read.');
|
|
344
346
|
const hex = colour => {
|
|
345
347
|
if (!colours.has(colour)) {
|
|
346
348
|
// CSS may resolve an accent to oklch/color-mix. The browser supplies its
|
|
@@ -671,13 +673,13 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
671
673
|
return {source, filename: name, kind, bom, ...parts, comparisonBase};
|
|
672
674
|
}
|
|
673
675
|
|
|
674
|
-
function _rapierSharedPageFallbackCss() {
|
|
675
|
-
|
|
676
|
+
function _rapierSharedPageFallbackCss(clearOnly = false) {
|
|
677
|
+
const clear = '.rapier-page>:is(pre,table,hr,figure,.table-scroll-wrap,.math-display-wrap){clear:both}\n';
|
|
678
|
+
return clearOnly ? clear : `
|
|
676
679
|
.rapier-page{display:flow-root}
|
|
677
680
|
.rapier-page>:first-child{margin-top:0}
|
|
678
681
|
.md-render h1{display:flow-root}
|
|
679
|
-
|
|
680
|
-
`;
|
|
682
|
+
` + clear;
|
|
681
683
|
}
|
|
682
684
|
|
|
683
685
|
function _rapierShareWrapKind(element) {
|
|
@@ -39,7 +39,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
|
|
|
39
39
|
for (const span of p.slice(0, 2)) { span[0] += shift; span[1] += shift; }
|
|
40
40
|
}
|
|
41
41
|
// Adjacent blocks deleted or inserted together: when the first carries the line break before it, each carries
|
|
42
|
-
// its own, so any one restores apart from its
|
|
42
|
+
// its own, so any one restores apart from its neighbors.
|
|
43
43
|
if (!raw) for (let i = 1; i < c.result.length; i++) {
|
|
44
44
|
const p = c.result[i - 1], q = c.result[i], kind = p[3][0], side = kind ? 1 : 0, text = side ? after : before;
|
|
45
45
|
if (kind % 2 || q[3][0] !== kind || p[2] || q[2] || p[side][1] !== q[side][0] || p[1 - side][0] !== q[1 - side][0]) continue;
|
|
@@ -109,7 +109,7 @@ export function walker(c) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Unequal lines pair in order by shared words. A line sharing under a quarter of its words with every candidate is
|
|
112
|
-
// replaced whole: an inserted line never borrows a
|
|
112
|
+
// replaced whole: an inserted line never borrows a neighbor's words, and a rewrite never reads as stray fragments.
|
|
113
113
|
function lines(x, y, context) {
|
|
114
114
|
const n = x.length - 1, m = y.length - 1;
|
|
115
115
|
if (n * m > 4096 || !spend(n * m + x[n] - x[0] + y[m] - y[0])) return row(x[0], x[n], y[0], y[m], context);
|
|
@@ -10,6 +10,91 @@ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
|
10
10
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
11
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
12
12
|
|
|
13
|
+
// Before/after are points; line is a multiple of the element's own type size.
|
|
14
|
+
// Presets and native exports read this same table. Explicit measures do not grow with font size.
|
|
15
|
+
export const SPACING_TYPES = Object.freeze(['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
16
|
+
'list', 'item', 'table', 'picture', 'drawing', 'quote', 'code', 'rule', 'footnote', 'toc']);
|
|
17
|
+
export const SPACING_PRESETS = Object.freeze({compact: .7, standard: 1, roomy: 1.5});
|
|
18
|
+
const SPACING_SELECTORS = Object.freeze({paragraph: 'p', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6',
|
|
19
|
+
list: ':is(ul,ol)', item: 'li', table: ':is(table,.table-scroll-wrap)', picture: 'p[data-md-picture]',
|
|
20
|
+
drawing: ':is(.diagram-block,p[data-md-picture]:has(img[data-md-svg],img[src^="data:image/svg+xml"]))', quote: 'blockquote',
|
|
21
|
+
code: 'pre', rule: 'hr:not(.footnotes-sep):not([data-md-break])', footnote: '.footnotes', toc: '.rapier-jump-list'});
|
|
22
|
+
const HEADING_LINE = [2 / 2.6, 1.5 / 1.9, 1.2 / 1.5, 1 / 1.25, .9 / 1.1, .82];
|
|
23
|
+
|
|
24
|
+
export function normalizeDocumentSpacing(value) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
if (Object.hasOwn(SPACING_PRESETS, value)) value = {preset: value};
|
|
27
|
+
else { try { value = JSON.parse(value); } catch (_) { return null; } }
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
32
|
+
if (key === 'preset') {
|
|
33
|
+
if (!Object.hasOwn(SPACING_PRESETS, entry)) return null;
|
|
34
|
+
result.preset = entry; continue;
|
|
35
|
+
}
|
|
36
|
+
if (!SPACING_TYPES.includes(key) || !entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
|
37
|
+
const row = {};
|
|
38
|
+
for (const [field, number] of Object.entries(entry)) {
|
|
39
|
+
if (!['before', 'after', 'line'].includes(field) || typeof number !== 'number' || !Number.isFinite(number) ||
|
|
40
|
+
number < (field === 'line' ? .5 : 0) || number > (field === 'line' ? 4 : 720)) return null;
|
|
41
|
+
row[field] = number;
|
|
42
|
+
}
|
|
43
|
+
result[key] = Object.freeze(row);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function documentSpacing(settings, kind = 'paragraph') {
|
|
49
|
+
const type = SPACING_TYPES.includes(kind) ? kind : 'paragraph', spacing = settings?.spacing;
|
|
50
|
+
const preset = spacing?.preset || 'standard', scale = SPACING_PRESETS[preset];
|
|
51
|
+
const size = (settings?.fontsize?.unit || 1.1) / 1.1;
|
|
52
|
+
const heading = /^h([1-6])$/.exec(type), leading = settings?.linestretch?.factor ??
|
|
53
|
+
(preset === 'compact' ? 1.3 : preset === 'roomy' ? 1.85 : DOCUMENT_LEADING);
|
|
54
|
+
const before = heading ? 15 : type === 'footnote' ? 9 : 0;
|
|
55
|
+
const after = heading ? 3 : type === 'paragraph' ? 6 : type === 'item' ? 3 : type === 'footnote' ? 0 : 9;
|
|
56
|
+
const line = heading ? leading * HEADING_LINE[Number(heading[1]) - 1] : type === 'code' ? leading * .75 / .7386 : leading;
|
|
57
|
+
if (settings?.word) return {before: (heading ? 12 : type === 'footnote' ? 9 : 0) * scale,
|
|
58
|
+
after: (heading ? 6 : type === 'paragraph' ? 8 : type === 'item' || type === 'footnote' ? 0 : 9) * scale,
|
|
59
|
+
line: leading, ...spacing?.[type]};
|
|
60
|
+
return {before: before * scale * size, after: after * scale * size, line, ...spacing?.[type]};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Word's widely supported Calibri/11 pt Normal template. Explicit document settings always win.
|
|
64
|
+
export function wordDocumentSettings(settings) {
|
|
65
|
+
return {...settings, word: true, fontsize: settings?.fontsize || fontSize('11pt'),
|
|
66
|
+
mainfont: settings?.mainfont || documentFont('Calibri'), sansfont: settings?.sansfont || documentFont('Calibri'),
|
|
67
|
+
linestretch: settings?.linestretch || leading('1.08')};
|
|
68
|
+
}
|
|
69
|
+
export function documentHeadingSize(settings, level) {
|
|
70
|
+
const body = (settings?.fontsize?.unit || 1.1) * 12;
|
|
71
|
+
return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.6,1.9,1.5,1.25,1.1,1])[level - 1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function documentSpacingCss(settings, scope = '.md-render', defaults = false) {
|
|
75
|
+
if (!defaults && !settings?.spacing && !settings?.linestretch && !settings?.fontsize) return '';
|
|
76
|
+
const length = points => 'calc(' + Number((points / 12).toFixed(8)) + ' * var(--md-unit))';
|
|
77
|
+
let css = '';
|
|
78
|
+
for (const type of SPACING_TYPES) {
|
|
79
|
+
const row = documentSpacing(settings, type), selector = scope + ' ' + SPACING_SELECTORS[type];
|
|
80
|
+
css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
|
|
81
|
+
if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
|
|
82
|
+
}
|
|
83
|
+
// Compound blocks own the outside gap; the last child does not add it again.
|
|
84
|
+
css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
|
|
85
|
+
scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
|
|
86
|
+
scope + ' .table-scroll-wrap>table{margin-block:0}' +
|
|
87
|
+
scope + ' pre{font-size:calc(var(--md-text-body) * .7386)}' +
|
|
88
|
+
scope + ' pre code{line-height:inherit}' + scope + ' .footnotes{padding-top:0}' +
|
|
89
|
+
scope + ' .footnotes-list{margin-block:0}' + scope + ' blockquote:not(.callout){padding-block:0}';
|
|
90
|
+
const item = documentSpacing(settings, 'item');
|
|
91
|
+
css += scope + ' ul.rapier-jump-list>li{padding-block:0;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
|
|
92
|
+
scope + ' ul.rapier-jump-list>li:last-child{margin-bottom:0}';
|
|
93
|
+
for (const [type, selector] of [['item','li>p'], ['table',':is(td,th)>p'], ['quote','blockquote>p'], ['footnote','.footnotes p'], ['toc','.rapier-jump-list li']])
|
|
94
|
+
css += scope + ' ' + selector + '{line-height:' + documentSpacing(settings, type).line + '}';
|
|
95
|
+
return css;
|
|
96
|
+
}
|
|
97
|
+
|
|
13
98
|
function measure(value, positive = false) {
|
|
14
99
|
const text = unquote(value);
|
|
15
100
|
const number = NUMBER.test(text) ? Number(text) : NaN;
|
|
@@ -40,7 +125,7 @@ const PAPERS = Object.freeze({
|
|
|
40
125
|
});
|
|
41
126
|
const EMPTY = Object.freeze({
|
|
42
127
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
43
|
-
papersize: null, geometry: null, pagestyle: null,
|
|
128
|
+
papersize: null, geometry: null, pagestyle: null, spacing: null,
|
|
44
129
|
});
|
|
45
130
|
|
|
46
131
|
function unquote(value) {
|
|
@@ -257,6 +342,7 @@ export function readDocumentSettings(markdown) {
|
|
|
257
342
|
return Object.freeze({
|
|
258
343
|
title: take('title') || null, subtitle: take('subtitle') || null, lang: languageTag(take('lang')),
|
|
259
344
|
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
|
|
345
|
+
spacing: normalizeDocumentSpacing(take('spacing')),
|
|
260
346
|
pagestyle: page === 'plain' || page === 'empty' ? page : null,
|
|
261
347
|
});
|
|
262
348
|
}
|
|
@@ -276,14 +362,14 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
276
362
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
277
363
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
278
364
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
279
|
-
if (!font && !['fontsize', 'linestretch'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
365
|
+
if (!font && !['fontsize', 'linestretch', 'spacing'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
280
366
|
const matches = fields.filter(item => item.key === key);
|
|
281
367
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
282
368
|
const field = matches[0];
|
|
283
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : leading(supplied);
|
|
369
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : leading(supplied);
|
|
284
370
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
285
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : String(value.factor);
|
|
286
|
-
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : before.linestretch.factor === value.factor)) continue;
|
|
371
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : String(value.factor);
|
|
372
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : before.linestretch.factor === value.factor)) continue;
|
|
287
373
|
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
288
374
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
289
375
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
|
@@ -327,7 +413,7 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
327
413
|
if (sans) decls.push('--md-font-sans:' + (sans.css || fontFamilyCss(sans.family)));
|
|
328
414
|
if (main) decls.push('font-family:' + (main.css || fontFamilyCss(main.family)));
|
|
329
415
|
if (mono) decls.push('--md-font-mono:' + (mono.css || fontFamilyCss(mono.family)));
|
|
330
|
-
return decls.length ? '.md-render{' + decls.join(';') + '}' : '';
|
|
416
|
+
return (decls.length ? '.md-render{' + decls.join(';') + '}' : '') + documentSpacingCss(settings);
|
|
331
417
|
}
|
|
332
418
|
|
|
333
419
|
// Null when the page keys do not change the print sheet. The view never asks.
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Pure text and markdown-it tokens: no DOM, no byte decoding, no globalThis.Rapier* (MIT kit; decoders stay in images/assets.mjs).
|
|
3
|
+
import {isWordSidecarBlock} from './word-sidecar.mjs';
|
|
3
4
|
|
|
4
5
|
export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
|
|
5
6
|
const parsers = new WeakMap();
|
|
@@ -93,6 +94,11 @@ const unheldLines = new Proxy(Array.prototype, {
|
|
|
93
94
|
// lines begins (undefined when the source ends before it). `env` is the whole pass's. The tables hold at most
|
|
94
95
|
// the lines between two visits: BLOCK_WINDOW of them, or as many as one block's rules read.
|
|
95
96
|
export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
|
|
97
|
+
for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The same tokenizer can pause between complete top-level windows; no grammar rule is suspended.
|
|
101
|
+
export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
|
|
96
102
|
const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
|
|
97
103
|
// The body is the source with every CRLF one unit shorter: count the pairs before a body offset, from the last
|
|
98
104
|
// offset asked.
|
|
@@ -105,7 +111,7 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
105
111
|
const tokens = [];
|
|
106
112
|
let origin = 0, first = 0, state;
|
|
107
113
|
// A line begins after the newline that ends the line before it. No rule writes eMarks.
|
|
108
|
-
const report = () =>
|
|
114
|
+
const report = () => ({tokens, firstLine: first, lineStart: line => {
|
|
109
115
|
if (!line) return sourceOffset(origin);
|
|
110
116
|
const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
|
|
111
117
|
return end < body.length ? sourceOffset(end + 1) : undefined;
|
|
@@ -115,7 +121,7 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
115
121
|
if (lines <= BLOCK_WINDOW) {
|
|
116
122
|
state = new parser.block.State(body, parser, env, tokens);
|
|
117
123
|
parser.block.tokenize(state, state.line, state.lineMax);
|
|
118
|
-
|
|
124
|
+
yield report(); return;
|
|
119
125
|
}
|
|
120
126
|
// The parser's own count of a last line that no newline ends: it does not count one of only spaces.
|
|
121
127
|
state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
|
|
@@ -180,14 +186,14 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
180
186
|
else if (!parser.utils.isSpace(code)) break;
|
|
181
187
|
}
|
|
182
188
|
frontier = origin; ended = false; betweenBlocks = false;
|
|
183
|
-
state.line = 0; state.lineMax = lines - first;
|
|
189
|
+
state.line = 0; state.lineMax = lines - first; state.rapierFirstLine = first;
|
|
184
190
|
const outer = unheldLine;
|
|
185
191
|
unheldLine = answer;
|
|
186
192
|
let finished = true;
|
|
187
193
|
try { parser.block.tokenize(state, 0, state.lineMax); }
|
|
188
194
|
catch (error) { if (error !== WINDOW_END) throw error; finished = false; }
|
|
189
195
|
finally { unheldLine = outer; }
|
|
190
|
-
report();
|
|
196
|
+
yield report();
|
|
191
197
|
if (finished) return;
|
|
192
198
|
tokens.length = 0;
|
|
193
199
|
origin = state.eMarks[state.line - 1] + 1; first += state.line;
|
|
@@ -209,6 +215,19 @@ export function dataImage(url) {
|
|
|
209
215
|
|
|
210
216
|
// Markdown-it owns recognition, nesting, duplicate precedence and escapes; block parse only.
|
|
211
217
|
export function parseAssets(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
218
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
219
|
+
let next = steps.next();
|
|
220
|
+
while (!next.done) next = steps.next();
|
|
221
|
+
return next.value;
|
|
222
|
+
}
|
|
223
|
+
export async function parseAssetsWork(source, factory, bodyOffset, work) {
|
|
224
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
225
|
+
work.signal?.throwIfAborted();
|
|
226
|
+
let next = steps.next();
|
|
227
|
+
while (!next.done) { await work.yield(); work.signal?.throwIfAborted(); next = steps.next(); }
|
|
228
|
+
return next.value;
|
|
229
|
+
}
|
|
230
|
+
function* assetSteps(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
212
231
|
if (typeof source !== 'string') return fail('image_source_invalid');
|
|
213
232
|
if (source.length > IMAGE_LIMITS.sourceChars) return fail('image_source_limit');
|
|
214
233
|
if (!source.includes(']:')) return {assets: new Map(), blocks: [], duplicateLabels: new Set(), appendixStart: source.length, references: Object.create(null)};
|
|
@@ -218,7 +237,7 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
218
237
|
if (definition) return definition.index;
|
|
219
238
|
const parser = parserFor(factory), env = {};
|
|
220
239
|
const assets = new Map(), blocks = [], seen = new Set(), duplicateLabels = new Set();
|
|
221
|
-
|
|
240
|
+
for (const {tokens, lineStart} of blockWindows(source, bodyOffset, parser, env)) {
|
|
222
241
|
for (const token of tokens) {
|
|
223
242
|
if (token.type !== 'reference_definition' || !token.map) continue;
|
|
224
243
|
const id = token.meta.label, definition = token.meta.mdImageDefinition, first = !seen.has(id);
|
|
@@ -241,7 +260,8 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
241
260
|
blocks.push(row);
|
|
242
261
|
if (first) assets.set(id, row);
|
|
243
262
|
}
|
|
244
|
-
|
|
263
|
+
yield;
|
|
264
|
+
}
|
|
245
265
|
const references = env.references || Object.create(null);
|
|
246
266
|
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
247
267
|
let appendixStart = source.length;
|
|
@@ -562,6 +582,7 @@ export function assetOmissions(source) {
|
|
|
562
582
|
return spans;
|
|
563
583
|
}
|
|
564
584
|
export function isAssetBlock(raw) {
|
|
585
|
+
if (isWordSidecarBlock(raw)) return true;
|
|
565
586
|
if (typeof raw !== 'string' || !/data:image\//i.test(raw)) return false;
|
|
566
587
|
const parsed = parseAssets(raw);
|
|
567
588
|
if (!parsed.blocks.length) return false;
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -169,9 +169,9 @@ export function wrapTextBlock(element) {
|
|
|
169
169
|
|
|
170
170
|
// The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
|
|
171
171
|
// metadata and empty paragraphs; any other block is a barrier in that direction.
|
|
172
|
-
export function wrapNeighbour(node, classify) {
|
|
173
|
-
for (const
|
|
174
|
-
for (let sibling = step(node); sibling; sibling = step(sibling)) {
|
|
172
|
+
export function wrapNeighbour(node, classify, step = (n, direction) => n && n[direction > 0 ? 'nextElementSibling' : 'previousElementSibling']) {
|
|
173
|
+
for (const direction of [1, -1]) {
|
|
174
|
+
for (let sibling = step(node, direction); sibling; sibling = step(sibling, direction)) {
|
|
175
175
|
const kind = classify(sibling);
|
|
176
176
|
if (kind && typeof kind === 'object') return kind.stop;
|
|
177
177
|
if (kind === 'metadata') continue;
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -10,10 +10,10 @@ const inkReader = () => {
|
|
|
10
10
|
const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
|
|
11
11
|
const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
|
|
12
12
|
|
|
13
|
-
// Ink (docs/markdown-standard.md, "Ink"): a pen stroke that stays with the words it marks, a paired comment with the
|
|
13
|
+
// Ink (docs/markdown-standard.md, "Ink"): a pen stroke that stays with the words it marks, a paired comment with the color span's shape. The
|
|
14
14
|
// opener holds the stroke:
|
|
15
15
|
// <!--ink KIND [NUMBER] [COLOUR] [w=N] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
|
|
16
|
-
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the
|
|
16
|
+
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the color span's value, red when absent; box is the frame's
|
|
17
17
|
// 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
|
|
18
18
|
// 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
|
|
19
19
|
// 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,
|
|
@@ -338,7 +338,7 @@ export function formatInkOpen(mark) {
|
|
|
338
338
|
let colour = '';
|
|
339
339
|
if (mark.hex != null) {
|
|
340
340
|
const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
|
|
341
|
-
if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the
|
|
341
|
+
if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the color is the color span\'s value');
|
|
342
342
|
colour = ' ' + value;
|
|
343
343
|
}
|
|
344
344
|
if (mark.width != null && (!Number.isInteger(mark.width) || mark.width < 2 || mark.width > 24)) throw new TypeError('ink: the width is an integer from 2 to 24, and 9 is spelled by leaving it out');
|
|
@@ -442,7 +442,7 @@ export function pairInkSpans(source, markers = null) {
|
|
|
442
442
|
return pairSpanMarkers(source, 'ink', markers);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
//
|
|
445
|
+
// Color keeps its first opener; ink pairs each closer with the innermost opener. The shared range
|
|
446
446
|
// shape lets custody owners keep every independent stroke, including strokes on identical words.
|
|
447
447
|
export function pairMarkers(source, kind, markers = null) {
|
|
448
448
|
const paired = pairSpanMarkers(source, kind, markers);
|
package/dist/spec/md-source.mjs
CHANGED
|
@@ -115,9 +115,13 @@ export function markdownSourcePositions(source, parser) {
|
|
|
115
115
|
|
|
116
116
|
// Capture only table-cell provenance while the parser still owns container-adjusted row
|
|
117
117
|
// bounds. Looking for cell text in a whole row aliases literal pipes and quote/list markers.
|
|
118
|
-
export function markdownTableSources(source, parser, environment, lineStarts = null) {
|
|
118
|
+
export function markdownTableSources(source, parser, environment, lineStarts = null, parse = null) {
|
|
119
119
|
const cells = new WeakMap();
|
|
120
|
-
|
|
120
|
+
const read = owner => {
|
|
121
|
+
if (parse) return parse(owner, source, environment).then(tokens => ({tokens, cells}));
|
|
122
|
+
return {tokens: owner.parse(source, environment), cells};
|
|
123
|
+
};
|
|
124
|
+
if (!source.includes('|')) return read(parser);
|
|
121
125
|
const lines = lineStarts || [0];
|
|
122
126
|
if (!lineStarts) for (const row of source.matchAll(/\r\n?|\n/g)) lines.push(row.index + row[0].length);
|
|
123
127
|
const owner = Object.create(parser), block = Object.create(parser.block), ruler = Object.create(parser.block.ruler);
|
|
@@ -131,14 +135,14 @@ export function markdownTableSources(source, parser, environment, lineStarts = n
|
|
|
131
135
|
if (token.type === 'tr_open') {
|
|
132
136
|
const row = token.map[0], from = state.bMarks[row] + state.tShift[row];
|
|
133
137
|
const line = state.src.lastIndexOf('\n', from - 1) + 1;
|
|
134
|
-
cell = tableSource(state.src.slice(from, state.eMarks[row]), lines[row] + from - line);
|
|
138
|
+
cell = tableSource(state.src.slice(from, state.eMarks[row]), lines[row + (state.rapierFirstLine || 0)] + from - line);
|
|
135
139
|
} else if (token.type === 'inline' && cell) cells.set(token, cell(token.content));
|
|
136
140
|
else if (token.type === 'tr_close') cell = null;
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
return matched;
|
|
140
144
|
});
|
|
141
|
-
return
|
|
145
|
+
return read(owner);
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
function tableSource(row, base) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// An inert document asset, kept in Source and excluded from the reading surface.
|
|
3
|
+
// This is recognition only: interchange/docx.mjs admits the bounded JSON and native XML.
|
|
4
|
+
export const WORD_SIDECAR_OPEN = '<!-- rapier-word:v1\n';
|
|
5
|
+
export function isWordSidecarBlock(raw) {
|
|
6
|
+
return typeof raw === 'string' && /^<!-- rapier-word:v1\r?\n[^\r\n]*\r?\n-->[ \t\r\n]*$/.test(raw);
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
package/style.css
CHANGED
|
@@ -267,9 +267,9 @@
|
|
|
267
267
|
.md-render h2{font-size:calc(var(--md-text-body) * 1.9);line-height:calc(var(--md-line) * 1.5);font-weight:var(--md-fw-semibold)}
|
|
268
268
|
.md-render h3{font-size:calc(var(--md-text-body) * 1.5);line-height:calc(var(--md-line) * 1.5);font-weight:var(--md-fw-semibold)}
|
|
269
269
|
.md-render h4{font-size:calc(var(--md-text-body) * 1.25);font-weight:var(--md-fw-semibold)}
|
|
270
|
-
/* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/
|
|
270
|
+
/* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/color), never
|
|
271
271
|
the browser default. Size stays strictly monotonic below h4; h6 sits at the body's own size and
|
|
272
|
-
firms up weight and dims
|
|
272
|
+
firms up weight and dims color, since its size alone would read as body text. */
|
|
273
273
|
.md-render h5{font-size:calc(var(--md-text-body) * 1.1);font-weight:var(--md-fw-semibold)}
|
|
274
274
|
.md-render h6{font-size:var(--md-text-body);font-weight:var(--md-fw-semibold);color:var(--md-heading-muted-color)}
|
|
275
275
|
.md-render p{margin:0 0 var(--md-gap-paragraph)}
|
|
@@ -300,9 +300,9 @@
|
|
|
300
300
|
|
|
301
301
|
.md-render .task-list-item input[type="checkbox"] {
|
|
302
302
|
--md-task-box: clamp(14px, .92em, 24px);
|
|
303
|
-
/* A form control does not inherit
|
|
303
|
+
/* A form control does not inherit color on its own; the shell's own app sheet gives it one
|
|
304
304
|
(`input,textarea,select{...color:inherit}`) and an exported page does not carry that sheet, so
|
|
305
|
-
it says so here. The drawn box names its own
|
|
305
|
+
it says so here. The drawn box names its own colors, so this changes no pixel today -- it
|
|
306
306
|
stops the two surfaces disagreeing about a property, which is how the next rule that does
|
|
307
307
|
depend on it would go wrong quietly. */
|
|
308
308
|
color: inherit;
|
|
@@ -321,10 +321,10 @@
|
|
|
321
321
|
touch-action: manipulation;
|
|
322
322
|
}
|
|
323
323
|
/* The box and its tick are Feather's square and check-square, with squared corners: drawn on Feather's
|
|
324
|
-
24-unit grid with its 2-unit stroke, the box's corners
|
|
324
|
+
24-unit grid with its 2-unit stroke, the box's corners mitered square, and a ticked box opens at its
|
|
325
325
|
top right where the tick breaks out through it. The grid is a third wider than the box itself (the
|
|
326
326
|
box is units 3 to 21), so the painted square keeps exactly the place and size --rapier-task-box gave
|
|
327
|
-
it; the ink is --color-line through a mask, so the theme
|
|
327
|
+
it; the ink is --color-line through a mask, so the theme recolors it and an export needs nothing
|
|
328
328
|
more than the data: images its policy already admits. The three glyphs are written here once, and a
|
|
329
329
|
Notes card's check rows read these same three (rapier-notes.css: one SVG per icon across Rapier). */
|
|
330
330
|
:root,.md-render {
|
|
@@ -361,15 +361,15 @@
|
|
|
361
361
|
position:relative;
|
|
362
362
|
--md-marker:0px;
|
|
363
363
|
counter-increment:rapier-ol;
|
|
364
|
-
min-height:
|
|
365
|
-
padding-top:
|
|
364
|
+
min-height:0;
|
|
365
|
+
padding-top:0;
|
|
366
366
|
}
|
|
367
367
|
.md-render ol>li::before{
|
|
368
368
|
content:counter(rapier-ol);
|
|
369
369
|
position:absolute;
|
|
370
|
-
/* The circle is
|
|
371
|
-
inset-inline-start:calc(-2.75 * var(--md-unit) + var(--md-marker, 0px)); top:
|
|
372
|
-
width:
|
|
370
|
+
/* The circle is centered on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
|
|
371
|
+
inset-inline-start:calc(-2.75 * var(--md-unit) + var(--md-marker, 0px)); top:0;
|
|
372
|
+
width:min(2 * var(--md-unit), 1lh); height:min(2 * var(--md-unit), 1lh);
|
|
373
373
|
display:flex; align-items:center; justify-content:center;
|
|
374
374
|
border:2px solid var(--md-color-line);
|
|
375
375
|
border-radius:999px;
|
|
@@ -581,7 +581,7 @@
|
|
|
581
581
|
}
|
|
582
582
|
|
|
583
583
|
/* Page break: unobtrusive by design, the same weight as .section-fold-btn (rapier-editor.css) --
|
|
584
|
-
muted
|
|
584
|
+
muted color and .37 opacity, never the stronger --color-line the horizontal-rule variants use.
|
|
585
585
|
Invisible on paper: this sheet's print rule zeros this out and applies the actual break instead. */
|
|
586
586
|
.md-render [data-md-break]{
|
|
587
587
|
position:relative;
|
|
@@ -642,7 +642,7 @@
|
|
|
642
642
|
font-size: var(--md-text-sm);
|
|
643
643
|
color: var(--md-color-text-secondary);
|
|
644
644
|
}
|
|
645
|
-
/* A view preference can keep embedded SVGs on their original light-paper
|
|
645
|
+
/* A view preference can keep embedded SVGs on their original light-paper colors. */
|
|
646
646
|
html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowchart){color-scheme:light}
|
|
647
647
|
.md-render img{
|
|
648
648
|
max-width:100%;height:auto;
|
|
@@ -650,9 +650,6 @@ html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowcha
|
|
|
650
650
|
vertical-align:middle;
|
|
651
651
|
}
|
|
652
652
|
.md-render img[data-rapier-image-size]{width:min(var(--md-image-width),100%);height:auto}
|
|
653
|
-
/* AUTO stays relative to its column. Authored and gesture-owned inline sizing wins;
|
|
654
|
-
Plain reads the intrinsic attributes without rerendering ordinary image blocks. */
|
|
655
|
-
html:not([data-layout="plain"]) .md-render img[data-rapier-image-geometry="auto"]{width:min(var(--rapier-image-auto-width),var(--rapier-image-natural-width));height:auto}
|
|
656
653
|
.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)}
|
|
657
654
|
|
|
658
655
|
.md-render dl{margin:0 0 var(--md-gap-block)}
|
|
@@ -675,9 +672,9 @@ html:not([data-layout="plain"]) .md-render img[data-rapier-image-geometry="auto"
|
|
|
675
672
|
.md-render mark[data-rapier-highlight="blue"]{--md-highlight-color:#4f91f7}
|
|
676
673
|
.md-render mark[data-rapier-highlight="yellow"]{--md-highlight-color:#e6b91e}
|
|
677
674
|
.md-render mark[data-rapier-highlight="purple"]{--md-highlight-color:#9b6dea}
|
|
678
|
-
/* Text
|
|
675
|
+
/* Text color: the canonical (light) hex and its OKLCH-derived dark pair both ship as inline
|
|
679
676
|
custom properties on the run itself (editor/engine.js, §b) — this just picks between them the
|
|
680
|
-
same way as the document palette: the document
|
|
677
|
+
same way as the document palette: the document color scheme chooses the matching value. The five named swatches also carry a literal,
|
|
681
678
|
pre-computed --md-color-dark here, so the common case survives even a round-trip that kept
|
|
682
679
|
data-md-color but dropped style, exactly like highlight's own swatch constants. */
|
|
683
680
|
.md-render [data-md-color]{--md-color-dark:var(--md-color);color:light-dark(var(--md-color),var(--md-color-dark))}
|