rapier-markdown-kit 1.2.32 → 1.2.40
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/README.md +16 -10
- package/dist/agent/vendor/pretext/NOTICE +2 -2
- package/dist/agent/vendor/pretext/SOURCE.json +62 -78
- package/dist/agent/vendor/pretext/analysis.js +24 -6
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
- package/dist/agent/vendor/pretext/graphemes.js +1 -1
- package/dist/agent/vendor/pretext/han-kerning.js +1 -1
- package/dist/agent/vendor/pretext/layout.js +14 -11
- package/dist/agent/vendor/pretext/line-break.js +170 -195
- package/dist/agent/vendor/pretext/line-breaks.js +1 -7
- package/dist/agent/vendor/pretext/line-text.js +2 -2
- package/dist/agent/vendor/pretext/measurement.js +454 -113
- package/dist/agent/vendor/pretext/prepare.js +164 -244
- package/dist/agent/vendor/pretext/rich-inline.js +57 -37
- package/dist/agent/will.mjs +73 -0
- package/dist/kit/ledger/data.mjs +2 -2
- package/dist/kit/ledger/format.mjs +86 -42
- package/dist/kit/ledger/hash.mjs +43 -2
- package/dist/kit/ledger/history.mjs +6 -0
- package/dist/kit/ledger/inverse-source.mjs +6 -2
- package/dist/kit/ledger/journal-records.mjs +4 -2
- package/dist/kit/ledger/merge.mjs +3 -17
- package/dist/kit/ledger/text-merge.mjs +191 -28
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-markdown.mjs +133 -10
- package/dist/kit/render-print.mjs +3 -3
- package/dist/kit/render-sanitize.mjs +66 -23
- package/dist/kit/render.mjs +10 -10
- package/dist/kit/wiki.mjs +34 -0
- package/dist/kit/will.d.mts +3 -0
- package/dist/layout/hyphenation.mjs +6 -4
- package/dist/layout/line-plan.mjs +96 -0
- package/dist/layout/model.mjs +2 -1
- package/dist/layout/paragraph-fit.mjs +4 -4
- package/dist/layout/prepared-paragraph.mjs +19 -2
- package/dist/layout/typography.mjs +57 -63
- package/dist/spec/document-fonts.mjs +1 -0
- package/dist/spec/document-settings.mjs +1 -0
- package/dist/spec/html-blocks.mjs +10 -7
- package/dist/spec/md-assets.mjs +52 -12
- package/package.json +1 -1
- package/style.css +37 -28
- package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
|
@@ -464,6 +464,7 @@ export function createTypography() {
|
|
|
464
464
|
function updateProjection(projection, plan) {
|
|
465
465
|
for (const {node,property,line,sign} of projection.bindings) {
|
|
466
466
|
const value = sign * plan.lines[line][property] + 'px';
|
|
467
|
+
if (node.hasAttribute(boundary)) { if (node.style.getPropertyValue('--md-fit-protrude') !== value) node.style.setProperty('--md-fit-protrude', value); continue; }
|
|
467
468
|
const name = property === 'opening' ? 'marginInlineStart' : property === 'closing' ? 'marginInlineEnd' : 'wordSpacing';
|
|
468
469
|
if (node.style[name] !== value) node.style[name] = value;
|
|
469
470
|
}
|
|
@@ -487,7 +488,7 @@ export function createTypography() {
|
|
|
487
488
|
add(line.end);
|
|
488
489
|
}
|
|
489
490
|
if (line.kind === 'space') operations.push({start:line.end,end:line.next,boundary:true});
|
|
490
|
-
else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph});
|
|
491
|
+
else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph,line:lineIndex,protrudes:line.closing>0});
|
|
491
492
|
const first = words[line.firstWord], last = words[line.lastWord];
|
|
492
493
|
if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
|
|
493
494
|
if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
|
|
@@ -538,6 +539,7 @@ export function createTypography() {
|
|
|
538
539
|
const at=ends.get(insert.offset);
|
|
539
540
|
const marker=paragraph.ownerDocument.createElement('span');marker.setAttribute(boundary,insert.glyph?'hyphen':'');
|
|
540
541
|
if (insert.glyph) marker.style.setProperty('--md-fit-hyphen',JSON.stringify(insert.glyph));
|
|
542
|
+
if (insert.protrudes) bindings.push({node:marker,property:'closing',line:insert.line,sign:-1});
|
|
541
543
|
at.after(marker);
|
|
542
544
|
}
|
|
543
545
|
const projection = {bindings,topology:topology(plan)};
|
|
@@ -689,77 +691,69 @@ export function createTypography() {
|
|
|
689
691
|
slotPlans.set(paragraph, {key, plan});
|
|
690
692
|
return plan;
|
|
691
693
|
}
|
|
692
|
-
function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, justify, hyphenate}) {
|
|
694
|
+
function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, slotSequence, columnsFilled, justify, hyphenate}) {
|
|
693
695
|
if (!obstacles.length) return null;
|
|
694
696
|
const doc = paragraph.ownerDocument, words = scope(doc);
|
|
695
697
|
const source = items.map(item => ({...item, hyphenate}));
|
|
696
698
|
const geometry = words.prepare(paragraph, source, {policy: 'paragraph', language: options.language, whiteSpace: 'normal',
|
|
697
699
|
wordBreak: options.wordBreak, hanging: false, paragraphBreaks: [0, source.length]});
|
|
698
700
|
if (!geometry) return null;
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
701
|
+
const build = order => {
|
|
702
|
+
const found = slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, 'ltr', order);
|
|
703
|
+
if (!found?.multiple) return null;
|
|
704
|
+
const sequence = found.slots, y = found.end;
|
|
705
|
+
const prepared = fitter.prepare(geometry);
|
|
706
|
+
let slots = sequence, plan = null;
|
|
707
|
+
for (let pass = 0; prepared && pass < 8; pass++) {
|
|
708
|
+
const measures = slots.map(slot => Math.floor(slot.width * 64) / 64);
|
|
709
|
+
if (!measures.length) return null;
|
|
710
|
+
plan = words.fit(paragraph, geometry, known => fitter.solveSteps(prepared, width, {widths: measures, shrink: justify ? undefined : 0}, known));
|
|
711
|
+
if (!plan) return null;
|
|
712
|
+
const wide = new Set();
|
|
713
|
+
plan.lines.forEach((line, index) => {
|
|
714
|
+
if (index >= slots.length || line.firstWord !== line.lastWord || line.kind === 'soft' || line.kind === 'auto') return;
|
|
715
|
+
const word = geometry.segments[line.segment].words[line.firstWord];
|
|
716
|
+
if (line.start === word.start && line.end === word.end && word.width > measures[index] + .5) wide.add(slots[index]);
|
|
717
|
+
});
|
|
718
|
+
if (!wide.size) break;
|
|
719
|
+
slots = slots.filter(slot => !wide.has(slot)); plan = null;
|
|
707
720
|
}
|
|
708
|
-
|
|
709
|
-
for (const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
for (let index = 0; index < plan.lines.length; index++) {
|
|
735
|
-
const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
|
|
736
|
-
const spacing = justify ? line.wordSpacing : 0, fragments = [];
|
|
737
|
-
const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
|
|
738
|
-
let at = 0, previous = null;
|
|
739
|
-
while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
|
|
740
|
-
for (; at < source.length && offsets[at] < line.end; at++) {
|
|
741
|
-
let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
|
|
742
|
-
if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
|
|
743
|
-
if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
|
|
744
|
-
if (to <= from) continue;
|
|
745
|
-
const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
|
|
746
|
-
if (previous) {
|
|
747
|
-
fragment.gapItemIndex = previous.itemIndex;
|
|
748
|
-
if (previous.end < from) {
|
|
749
|
-
if (!space.test(text.slice(previous.end, from))) return null;
|
|
750
|
-
const before = segment.find(word => word.end === previous.end);
|
|
751
|
-
if (!before?.gap) return null;
|
|
752
|
-
fragment.gapBefore = before.gap.width + spacing;
|
|
753
|
-
} else fragment.gapBefore = 0;
|
|
721
|
+
const perRow = new Map();
|
|
722
|
+
for (const slot of slots) perRow.set(slot.y, (perRow.get(slot.y) || 0) + 1);
|
|
723
|
+
if (!plan || ![...perRow.values()].some(count => count > 1)) return null;
|
|
724
|
+
const lines = [], text = geometry.text, offsets = geometry.offsets, space = /^[\t\n\f\r ]*$/;
|
|
725
|
+
for (let index = 0; index < plan.lines.length; index++) {
|
|
726
|
+
const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
|
|
727
|
+
const spacing = justify ? line.wordSpacing : 0, fragments = [];
|
|
728
|
+
const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
|
|
729
|
+
let at = 0, previous = null;
|
|
730
|
+
while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
|
|
731
|
+
for (; at < source.length && offsets[at] < line.end; at++) {
|
|
732
|
+
let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
|
|
733
|
+
if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
|
|
734
|
+
if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
|
|
735
|
+
if (to <= from) continue;
|
|
736
|
+
const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
|
|
737
|
+
if (previous) {
|
|
738
|
+
fragment.gapItemIndex = previous.itemIndex;
|
|
739
|
+
if (previous.end < from) {
|
|
740
|
+
if (!space.test(text.slice(previous.end, from))) return null;
|
|
741
|
+
const before = segment.find(word => word.end === previous.end);
|
|
742
|
+
if (!before?.gap) return null;
|
|
743
|
+
fragment.gapBefore = before.gap.width + spacing;
|
|
744
|
+
} else fragment.gapBefore = 0;
|
|
745
|
+
}
|
|
746
|
+
fragments.push(fragment); previous = {itemIndex: at, end: to};
|
|
754
747
|
}
|
|
755
|
-
fragments.
|
|
748
|
+
if (!fragments.length) return null;
|
|
749
|
+
if (line.kind === 'soft' || line.kind === 'auto') fragments.at(-1).glyph = line.glyph || '-';
|
|
750
|
+
lines.push({x: slot.x, y: slot.y, width: slot.width, fragments, wordSpacing: spacing});
|
|
756
751
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return {lines, height: last ? Math.max(lineHeight, last.y + lineHeight) : lineHeight};
|
|
752
|
+
return {plan: {lines, height: Math.max(lineHeight, ...lines.map(line => line.y + lineHeight))}, filled: columnsFilled(slots, slots.slice(0, plan.lines.length))};
|
|
753
|
+
};
|
|
754
|
+
const columns = build('columns');
|
|
755
|
+
if (!columns || columns.filled) return columns?.plan || null;
|
|
756
|
+
return build('rows')?.plan || columns.plan;
|
|
763
757
|
}
|
|
764
758
|
|
|
765
759
|
function retire(root, {documentChanged=false}={}) {
|
|
@@ -211,6 +211,7 @@ export function documentFontUsage(markdown, parser, defaultFont) {
|
|
|
211
211
|
const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
|
|
212
212
|
const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
|
|
213
213
|
const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
|
|
214
|
+
if (!main && !sans && !mono && !source.includes('<')) return use;
|
|
214
215
|
const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
|
|
215
216
|
let blockFamily = main;
|
|
216
217
|
for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
|
|
@@ -129,6 +129,7 @@ function paperOf(value) {
|
|
|
129
129
|
return pixels && width <= 10000 && height <= 10000 ? Object.freeze({name, width: width * 15, height: height * 15, css: width + 'px ' + height + 'px'}) : null;
|
|
130
130
|
}
|
|
131
131
|
export const pageSizeOf = settings => settings?.papersize || PAPERS.letter;
|
|
132
|
+
export const paperForLanguage = language => /(?:^|[-_])(?:US|CA|MX|PH|CL|CO|VE|CR|GT)$/i.test(String(language || '')) ? PAPERS.letter : PAPERS.a4;
|
|
132
133
|
export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
|
|
133
134
|
const EMPTY = Object.freeze({
|
|
134
135
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
@@ -425,7 +425,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
425
425
|
const name = attr.name.toLowerCase(), value = attr.value;
|
|
426
426
|
if (name.startsWith('on') || name.startsWith('data-rapier-') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(name)) el.removeAttribute(attr.name);
|
|
427
427
|
if (['src','srcset','href'].includes(attr.localName.toLowerCase()) && !(name.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(value) ||
|
|
428
|
-
(name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:png|jpeg|webp|gif|jxl|svg\+xml)
|
|
428
|
+
(name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);|svg\+xml,)/i.test(value))) el.removeAttribute(attr.name);
|
|
429
429
|
}
|
|
430
430
|
if (tag.startsWith('animate') || tag === 'set') {
|
|
431
431
|
const attribute = (el.getAttribute('attributeName') || '').trim().toLowerCase().split(':').pop();
|
|
@@ -437,13 +437,16 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
437
437
|
const name = tag === 'img' ? 'src' : el.hasAttribute('href') ? 'href' : 'xlink:href', original = el.getAttribute(name);
|
|
438
438
|
if (original) {
|
|
439
439
|
const match = /^data:(image\/(?:png|jpeg|webp|gif|jxl|svg\+xml));(?:charset=[^;,]+;)?base64,([A-Za-z0-9+/]+={0,2})$/i.exec(original);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
440
|
+
const percent = /^data:image\/svg\+xml,/i.test(original);
|
|
441
|
+
if ((!match && !percent) || original.length > 24 * 1024 * 1024) throw new Error('Image requires a bounded base64 data URL');
|
|
442
|
+
const mime = percent ? 'image/svg+xml' : match[1].toLowerCase();
|
|
443
|
+
const url = percent ? original : 'data:' + mime + ';base64,' + match[2];
|
|
444
|
+
const bytes = percent ? new win.TextEncoder().encode(decodeURIComponent(original.slice(19)))
|
|
445
|
+
: Uint8Array.from(win.atob(match[2]), c => c.charCodeAt(0));
|
|
446
|
+
const info = mime === 'image/gif' ? media.inspectGIF(bytes) : imageInfo(url) || admitted[url];
|
|
447
|
+
if (!info && mime !== 'image/svg+xml') {el.removeAttribute(name); continue;}
|
|
445
448
|
if (info) {pixels += info.width * info.height; if (pixels > 24000000) throw new Error('HTML image pixel budget'); admitted[url] = {width: info.width, height: info.height, animated: !!info.animated};}
|
|
446
|
-
if (
|
|
449
|
+
if (mime === 'image/svg+xml') {
|
|
447
450
|
const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
|
|
448
451
|
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
|
|
449
452
|
const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -17,9 +17,9 @@ export function installMarkdownImages(md) {
|
|
|
17
17
|
if (md.__rapierImagesInstalled) return md;
|
|
18
18
|
md.__rapierImagesInstalled = true;
|
|
19
19
|
const validate = md.validateLink;
|
|
20
|
-
md.validateLink = url =>
|
|
20
|
+
md.validateLink = url => !!dataImage(url) || validate(url);
|
|
21
21
|
const normalize = md.normalizeLink;
|
|
22
|
-
md.normalizeLink = url =>
|
|
22
|
+
md.normalizeLink = url => dataImage(url) ? url : normalize(url);
|
|
23
23
|
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
24
24
|
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
25
25
|
if (silent) return reference(state, start, end, silent);
|
|
@@ -179,10 +179,35 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
|
|
183
|
+
code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
|
|
184
|
+
const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
185
|
+
let lastScan = null;
|
|
182
186
|
export function dataImage(url) {
|
|
183
187
|
if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
|
|
188
|
+
if (url.length > 4096) {
|
|
189
|
+
if (lastScan?.url !== url) lastScan = {url, image: scanDataImage(url)};
|
|
190
|
+
return lastScan.image && {...lastScan.image};
|
|
191
|
+
}
|
|
192
|
+
return scanDataImage(url);
|
|
193
|
+
}
|
|
194
|
+
function scanDataImage(url) {
|
|
184
195
|
const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
|
|
185
|
-
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1
|
|
196
|
+
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1,
|
|
197
|
+
...(prepared.encoding ? {encoding: prepared.encoding} : {})};
|
|
198
|
+
if (/^data:image\/svg\+xml,/i.test(url)) {
|
|
199
|
+
const payloadStart = 19;
|
|
200
|
+
let byteLength = 0;
|
|
201
|
+
for (let at = payloadStart; at < url.length; at++, byteLength++) {
|
|
202
|
+
const code = url.charCodeAt(at);
|
|
203
|
+
if (code === 37) {
|
|
204
|
+
if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
|
|
205
|
+
at += 2;
|
|
206
|
+
} else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
|
|
207
|
+
}
|
|
208
|
+
return byteLength && byteLength <= IMAGE_LIMITS.bytes
|
|
209
|
+
? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
|
|
210
|
+
}
|
|
186
211
|
const match = /^data:(image\/(?:jxl|png|jpeg|webp|gif|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
187
212
|
if (!match || match[2].length % 4) return null;
|
|
188
213
|
const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
|
|
@@ -291,7 +316,8 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
|
|
|
291
316
|
preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
|
|
292
317
|
const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
|
|
293
318
|
for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
|
|
294
|
-
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength
|
|
319
|
+
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength,
|
|
320
|
+
...(row.encoding ? {encoding: row.encoding} : {})});
|
|
295
321
|
return {source: row.source, factory, image, index: {assets: new Map([[row.id, local]]), blocks: [local],
|
|
296
322
|
references: Object.assign(Object.create(null), {[row.id]: {href: row.url, title: row.title}}),
|
|
297
323
|
duplicateLabels: new Set(), appendixStart: 0}};
|
|
@@ -314,7 +340,7 @@ export function projectImageDefinitions(source, index) {
|
|
|
314
340
|
const references = Object.create(null);
|
|
315
341
|
for (const [label, definition] of Object.entries(index.references)) {
|
|
316
342
|
const image = index.assets.get(label);
|
|
317
|
-
references[label] = {href: image?.url === definition.href ? '
|
|
343
|
+
references[label] = {href: image?.url === definition.href ? image.url.slice(0, image.url.indexOf(',') + 1) + 'AA==' : definition.href,
|
|
318
344
|
title: definition.title};
|
|
319
345
|
}
|
|
320
346
|
return {source: records.length ? parts.join('') : source, records, references};
|
|
@@ -513,12 +539,17 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
|
513
539
|
!remaining.includes('[' + row.id) && !now.html.includes(row.id) && !now.html.includes(row.url.toUpperCase());
|
|
514
540
|
});
|
|
515
541
|
return retired.sort((a, b) => b.start - a.start).map(row => {
|
|
516
|
-
const gap = /(?:\r\n
|
|
542
|
+
const gap = /(?:\r\n|\r(?!\n)|(?<!\r)\n){2}$/.exec(after.slice(Math.max(0, row.start - 5), row.start));
|
|
517
543
|
let tail = after.slice(row.end);
|
|
518
544
|
const last = /^(?:\r\n|\n|\r)?$/.test(tail);
|
|
519
545
|
for (const other of retired) if (other.start >= row.end) tail = tail.replace(other.source, '');
|
|
520
546
|
const whole = gap && !/\S/.test(tail) && row.start - gap[0].length > markdownBodyOffset(after);
|
|
521
|
-
|
|
547
|
+
if (whole) {
|
|
548
|
+
const [first, second] = gap[0].match(/\r\n|\r|\n/g), padding = first === second ? gap[0] : second;
|
|
549
|
+
return {pos: row.start - padding.length, removed: padding + row.source + (last ? after.slice(row.end) : ''), inserted: ''};
|
|
550
|
+
}
|
|
551
|
+
const between = gap && /^(?:\r\n|\r(?!\n)|(?<!\r)\n){2}/.test(tail) && row.start - gap[0].length >= markdownBodyOffset(after);
|
|
552
|
+
return between ? {pos: row.start - gap[0].length, removed: gap[0] + row.source, inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
|
|
522
553
|
});
|
|
523
554
|
} catch (_) { return []; }
|
|
524
555
|
}
|
|
@@ -551,7 +582,7 @@ function previousOmissions(view, kind) {
|
|
|
551
582
|
rows = rows.flatMap(row => {
|
|
552
583
|
const permitted = kind === 'asset' ? /^[A-Za-z0-9+/=\\;&%#._~-]*$/
|
|
553
584
|
: row.profile === 'base64' ? /^[A-Za-z0-9+/=\s]*$/ : /^[A-Za-z0-9\-._~!$&*+;=:@/?%]*$/;
|
|
554
|
-
if (change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
|
|
585
|
+
if (!row.percent && change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
|
|
555
586
|
const next = {...row, end: row.end + delta, chars: row.chars + delta};
|
|
556
587
|
if (kind === 'asset') {
|
|
557
588
|
const was = payloadFacts(change.before.deref().slice(change.pos, end)), now = payloadFacts(change.text);
|
|
@@ -575,6 +606,7 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
575
606
|
const prior = previousOmissions(view, kind), rows = new Map();
|
|
576
607
|
const pattern = kind === 'asset' ? /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/iy
|
|
577
608
|
: /data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;[^,\s"'<>)]*)?,/iy;
|
|
609
|
+
const svgPattern = /data:(image\/svg\+xml),[A-Za-z0-9%\-._~:/?@+$,;=]*/iy;
|
|
578
610
|
let end = 0, envelopeIndex = 0;
|
|
579
611
|
for (const at of measured.markers) {
|
|
580
612
|
if (kind === 'asset' && at < end) continue;
|
|
@@ -583,15 +615,21 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
583
615
|
let row = prior.get(at);
|
|
584
616
|
if (!row) {
|
|
585
617
|
pattern.lastIndex = at;
|
|
586
|
-
|
|
618
|
+
let match = pattern.exec(source), percent = false;
|
|
619
|
+
if (!match && kind === 'asset') {
|
|
620
|
+
svgPattern.lastIndex = at; match = svgPattern.exec(source);
|
|
621
|
+
if (match && !dataImage(match[0])) match = null;
|
|
622
|
+
percent = !!match;
|
|
623
|
+
if (percent) pattern.lastIndex = svgPattern.lastIndex;
|
|
624
|
+
}
|
|
587
625
|
if (!match) continue;
|
|
588
626
|
if (kind === 'asset') {
|
|
589
627
|
const value = match[0], payload = value.slice(value.indexOf(',') + 1);
|
|
590
628
|
const {bad, equals} = payloadFacts(payload);
|
|
591
|
-
const bytes = payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
|
|
629
|
+
const bytes = percent ? dataImage(value)?.byteLength ?? null : payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
|
|
592
630
|
row = {start: at, end: pattern.lastIndex, chars: value.length,
|
|
593
631
|
domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes,
|
|
594
|
-
payloadStart: pattern.lastIndex - payload.length, bad, equals};
|
|
632
|
+
payloadStart: pattern.lastIndex - payload.length, bad, equals, ...(percent ? {percent} : {})};
|
|
595
633
|
} else {
|
|
596
634
|
const payloadStart = pattern.lastIndex, base64 = /;base64,/i.test(match[0]);
|
|
597
635
|
let to = payloadStart;
|
|
@@ -608,7 +646,7 @@ function dataMatches(source, kind, envelopes = []) {
|
|
|
608
646
|
}
|
|
609
647
|
export function assetOmissions(source) {
|
|
610
648
|
if (typeof source !== 'string') return [];
|
|
611
|
-
return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, ...row}) => row);
|
|
649
|
+
return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, percent, ...row}) => row);
|
|
612
650
|
}
|
|
613
651
|
|
|
614
652
|
export function dataOmissions(source) {
|
|
@@ -667,3 +705,5 @@ export function appendAssetText(source, asset, lineEnding, {preserveLabel = fals
|
|
|
667
705
|
return {source: nextSource, id: asset.id, reference: asset.label, added: true, suffix};
|
|
668
706
|
}
|
|
669
707
|
export async function appendAsset(source, asset) { return appendAssetText(source, asset); }
|
|
708
|
+
|
|
709
|
+
export const imageOnlySourceBlock = source => /^(?:!\[[^\]\n]*\]\(data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);base64,[A-Za-z0-9+\/=]+|svg\+xml,[A-Za-z0-9%\-._~:/?@+$,;=]+)(?: "[^"\n]*")?\)(?:[ \t]*<!--md-layout:v1 [^>\r\n]{1,180}-->)?\s*)+$/.test(source);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.40",
|
|
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
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
.md-render p[data-rapier-typography-fit]{text-align:justify!important;text-align-last:start;text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
|
|
143
143
|
.md-render p[data-rapier-typography-fit] :is(a,span,em,i,strong,b,del,s,mark,code,kbd,samp,u,ins,abbr){text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
|
|
144
144
|
.md-render [data-rapier-typography-separator]{font-size:0;line-height:0;word-spacing:0!important;letter-spacing:0!important}
|
|
145
|
-
.md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-')}
|
|
145
|
+
.md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-');margin-inline-end:var(--md-fit-protrude,0px)}
|
|
146
146
|
.md-render p[data-rapier-typography-ragged]{text-align:start!important}
|
|
147
147
|
.md-render p[data-md-lines]:not([data-rapier-typography-protected-initial])::first-letter{
|
|
148
148
|
initial-letter:var(--md-drop-lines,normal);
|
|
@@ -219,29 +219,31 @@
|
|
|
219
219
|
at admission; only renderer paint omissions are corrected here. */
|
|
220
220
|
:where(.md-render) svg.rapier-native-flowchart{display:block;margin-inline:auto;height:auto}
|
|
221
221
|
:where(.md-render) svg.rapier-diagram{display:block;margin-inline:auto;height:auto;text-rendering:optimizeLegibility}
|
|
222
|
-
/* The same token bridge serves the document, live preview and portable page.
|
|
222
|
+
/* The same token bridge serves the document, live preview and portable page. Pele reads --pele-* names; the
|
|
223
|
+
adapter (_presentPele in shell/plugin-loader.js) points them at these --md-pele-* names, and the app's own sheet
|
|
224
|
+
lets its theme (--color-*, --font-*) lead them in the editor. */
|
|
223
225
|
:where(.md-render) :is(.diagram-block,.diagram-live-preview){
|
|
224
|
-
--pele-bg:transparent;
|
|
225
|
-
--pele-fg:var(--
|
|
226
|
-
--pele-line:var(--
|
|
227
|
-
--pele-muted:var(--
|
|
228
|
-
--pele-surface:var(--
|
|
229
|
-
--pele-surface-alt:var(--
|
|
230
|
-
--pele-border:var(--
|
|
231
|
-
--pele-accent:var(--
|
|
232
|
-
--pele-font:var(--
|
|
233
|
-
--pele-font-mono:var(--
|
|
234
|
-
--pele-radius:0;
|
|
235
|
-
--pele-heading-weight:var(--md-fw-semibold);
|
|
236
|
-
--pele-title-weight:var(--md-fw-semibold);
|
|
237
|
-
--pele-series-1:var(--pele-accent);
|
|
238
|
-
--pele-series-2:#4f91f7;
|
|
239
|
-
--pele-series-3:#35c57a;
|
|
240
|
-
--pele-series-4:#e6b91e;
|
|
241
|
-
--pele-series-5:#9b6dea;
|
|
242
|
-
--pele-series-6:#f05a62;
|
|
243
|
-
--pele-series-7:color-mix(in srgb,var(--pele-accent) 50%,#4f91f7);
|
|
244
|
-
--pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
|
|
226
|
+
--md-pele-bg:transparent;
|
|
227
|
+
--md-pele-fg:var(--md-color-text);
|
|
228
|
+
--md-pele-line:var(--md-color-text);
|
|
229
|
+
--md-pele-muted:var(--md-color-text-secondary);
|
|
230
|
+
--md-pele-surface:var(--md-color-surface-2);
|
|
231
|
+
--md-pele-surface-alt:var(--md-color-border-focus);
|
|
232
|
+
--md-pele-border:var(--md-color-border);
|
|
233
|
+
--md-pele-accent:var(--md-color-accent);
|
|
234
|
+
--md-pele-font:var(--md-font-sans);
|
|
235
|
+
--md-pele-font-mono:var(--md-font-mono);
|
|
236
|
+
--md-pele-radius:0;
|
|
237
|
+
--md-pele-heading-weight:var(--md-fw-semibold);
|
|
238
|
+
--md-pele-title-weight:var(--md-fw-semibold);
|
|
239
|
+
--md-pele-series-1:var(--md-pele-accent);
|
|
240
|
+
--md-pele-series-2:#4f91f7;
|
|
241
|
+
--md-pele-series-3:#35c57a;
|
|
242
|
+
--md-pele-series-4:#e6b91e;
|
|
243
|
+
--md-pele-series-5:#9b6dea;
|
|
244
|
+
--md-pele-series-6:#f05a62;
|
|
245
|
+
--md-pele-series-7:color-mix(in srgb,var(--md-pele-accent) 50%,#4f91f7);
|
|
246
|
+
--md-pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
/* Radar gives these SVG labels a color declaration; SVG text needs a fill. */
|
|
@@ -418,6 +420,7 @@
|
|
|
418
420
|
min-height:var(--md-ol-circle);
|
|
419
421
|
padding-top:min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line)));
|
|
420
422
|
}
|
|
423
|
+
.md-render{--md-ol-dot-line:radial-gradient(circle closest-side, var(--md-color-line) calc(100% - 0.5px), transparent 100%) center top / 3px 6px repeat-y}
|
|
421
424
|
.md-render ol>li::before{
|
|
422
425
|
content:counter(rapier-ol);
|
|
423
426
|
position:absolute;
|
|
@@ -441,16 +444,21 @@
|
|
|
441
444
|
|
|
442
445
|
inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
|
|
443
446
|
/* From just below this circle into the next one: at least to the next item's top (--md-item-gap is the gap between
|
|
444
|
-
items the spacing settings give), and at least a quarter line, which the next circle covers.
|
|
447
|
+
items the spacing settings give), and at least a quarter line, which the next circle covers. Where the circles
|
|
448
|
+
nearly touch (one-line items) there is no room for dots, so none are drawn: the line shows only when it spans at
|
|
449
|
+
least half a line (100% is this item's height). */
|
|
445
450
|
top:calc(var(--md-ol-top) + 1.0875 * var(--md-ol-circle));
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
--md-ol-dots:calc(100% - var(--md-ol-top) - 1.0875 * var(--md-ol-circle) + max(var(--md-item-gap, 0px), var(--md-line) / 4));
|
|
452
|
+
height:clamp(0px, (var(--md-ol-dots) - var(--md-line) / 2) * 9999, var(--md-ol-dots));
|
|
453
|
+
/* Round dots, 3px across every 6px: a dotted border draws squares in Chrome. */
|
|
454
|
+
width:3px;
|
|
455
|
+
background:var(--md-ol-dot-line);
|
|
448
456
|
z-index:0;
|
|
449
457
|
}
|
|
450
458
|
/* A heading between two parts of one numbered list (the second part's start follows the first's last number;
|
|
451
459
|
_rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
|
|
452
460
|
line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
|
|
453
|
-
.md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
|
|
461
|
+
.md-render ol[data-rapier-list-continues]>li:last-child::after{height:auto;bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
|
|
454
462
|
/* A wrapped picture set the next circle aside: the dotted line ends with this item, as at a list's last. */
|
|
455
463
|
.md-render ol>li[data-rapier-wrap-step]::after{content:none!important}
|
|
456
464
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
|
|
@@ -460,7 +468,8 @@
|
|
|
460
468
|
inset-inline-start:calc(calc(1 * var(--md-unit)) - 1.5px);
|
|
461
469
|
top:0;
|
|
462
470
|
bottom:calc(-1 * var(--md-list-heading-after,var(--md-gap-heading-after)));
|
|
463
|
-
|
|
471
|
+
width:3px;
|
|
472
|
+
background:var(--md-ol-dot-line);
|
|
464
473
|
z-index:0;
|
|
465
474
|
}
|
|
466
475
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]:has(+:is(h1,h2,h3,h4,h5,h6)){--md-list-heading-after:var(--md-gap-heading-before)}
|