rapier-markdown-kit 1.1.102 → 1.1.104
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 +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {markdownParser, installMarkdownImages, normalizeLabel} from './md-assets.mjs';
|
|
4
4
|
import {splitOpeningFrontmatter} from './frontmatter.mjs';
|
|
5
5
|
import {readDocumentSettings, documentFont} from './document-settings.mjs';
|
|
6
|
-
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
6
|
+
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS, typography} from './md-marks.mjs';
|
|
7
7
|
|
|
8
8
|
// Unquoted CSS generic families require no portable asset. Quoted names remain custom.
|
|
9
9
|
export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
@@ -188,6 +188,13 @@ function htmlFontFamilies(html, md) {
|
|
|
188
188
|
for (const tag of html.matchAll(/<[A-Za-z][A-Za-z0-9:-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
|
|
189
189
|
for (const attr of tag[0].matchAll(/\s(data-md-font|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
|
|
190
190
|
const value = decode(attr[2] ?? attr[3] ?? attr[4]), names = [];
|
|
191
|
+
if (/^<span(?=[\s>])/i.test(tag[0])) {
|
|
192
|
+
// Raw or unpaired spans use the same admission as paired typography. A CSS
|
|
193
|
+
// resource expression or authored data-* hook cannot request a font asset.
|
|
194
|
+
const family = attr[1].toLowerCase() === 'style' ? typography.parseStyle(value).family : null;
|
|
195
|
+
if (family?.custom) families.add(family.name);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
191
198
|
if (attr[1].toLowerCase() === 'data-md-font') names.push(value);
|
|
192
199
|
else for (const declared of value.matchAll(/(?:^|;)\s*font-family\s*:\s*((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^;])*)/gi)) {
|
|
193
200
|
const match = /^\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^,!]+))/.exec(declared[1]);
|
|
@@ -230,9 +237,11 @@ export function documentFontUsage(markdown, parser, defaultFont) {
|
|
|
230
237
|
for (const child of token.children || []) {
|
|
231
238
|
if (child.type === 'html_inline') html += child.content;
|
|
232
239
|
if (['text', 'text_special', 'emoji', 'image', 'code_inline'].includes(child.type)) text += child.content;
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
|
|
240
|
+
const typed = child.meta?.typography;
|
|
241
|
+
if (typed && child.nesting === 1) {
|
|
242
|
+
stack.push(typed.style.family ? typed.style.family.custom ? typed.style.family.name : null : stack.at(-1)); continue;
|
|
243
|
+
}
|
|
244
|
+
if (typed && child.nesting === -1) { if (stack.length > 1) stack.pop(); continue; }
|
|
236
245
|
if (child.type === 'code_inline') add(mono, child.content);
|
|
237
246
|
else if (['text', 'text_special', 'emoji', 'image'].includes(child.type)) add(stack.at(-1), child.content);
|
|
238
247
|
}
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -73,6 +73,128 @@ function parserFor(factory = configured || globalThis.markdownit) {
|
|
|
73
73
|
export {parserFor as markdownParser};
|
|
74
74
|
export const markdownBodyOffset = source => splitSource ? splitSource(source).bodyOffset : 0;
|
|
75
75
|
|
|
76
|
+
// markdown-it's block state builds five tables with one entry per line of its whole input, and its rules index
|
|
77
|
+
// them by line. A long body gets the parser's one block pass with tables that hold a run of lines instead: a read
|
|
78
|
+
// past the lines they hold fills them further from the parser's own line scan, and they are emptied between
|
|
79
|
+
// top-level blocks. No line is parsed twice, so every token and `env` are the ones a whole-input state yields.
|
|
80
|
+
const BLOCK_WINDOW = 1024, TABLE_STEP = 128, WINDOW_END = Symbol('block window end');
|
|
81
|
+
const LINE_TABLES = ['bMarks', 'eMarks', 'tShift', 'sCount', 'bsCount'];
|
|
82
|
+
const lineIndex = key => typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key);
|
|
83
|
+
// An array reaches its prototype only for an index it does not hold. One prototype serves every pass, so the
|
|
84
|
+
// parser's table reads see one more array shape, not one per pass.
|
|
85
|
+
let unheldLine = null;
|
|
86
|
+
const unheldLines = new Proxy(Array.prototype, {
|
|
87
|
+
get: (target, key, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key)) : Reflect.get(target, key, table),
|
|
88
|
+
set: (target, key, value, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key), true, value) : Reflect.set(target, key, value, table),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// The parser's block pass over source.slice(bodyOffset). `visit` receives top-level blocks as they finish:
|
|
92
|
+
// `tokens` with line numbers counted from `firstLine`, and lineStart(line), the source offset where one of those
|
|
93
|
+
// lines begins (undefined when the source ends before it). `env` is the whole pass's. The tables hold at most
|
|
94
|
+
// the lines between two visits: BLOCK_WINDOW of them, or as many as one block's rules read.
|
|
95
|
+
export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
|
|
96
|
+
const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
|
|
97
|
+
// The body is the source with every CRLF one unit shorter: count the pairs before a body offset, from the last
|
|
98
|
+
// offset asked.
|
|
99
|
+
let pair = source.indexOf('\r\n', bodyOffset), shift = bodyOffset;
|
|
100
|
+
const sourceOffset = at => {
|
|
101
|
+
while (pair >= 0 && at + shift > pair) { shift++; pair = source.indexOf('\r\n', pair + 2); }
|
|
102
|
+
for (let last; shift > bodyOffset && at + shift - 1 <= (last = source.lastIndexOf('\r\n', (pair < 0 ? source.length : pair) - 1)); pair = last) shift--;
|
|
103
|
+
return at + shift;
|
|
104
|
+
};
|
|
105
|
+
const tokens = [];
|
|
106
|
+
let origin = 0, first = 0, state;
|
|
107
|
+
// A line begins after the newline that ends the line before it. No rule writes eMarks.
|
|
108
|
+
const report = () => visit({tokens, firstLine: first, lineStart: line => {
|
|
109
|
+
if (!line) return sourceOffset(origin);
|
|
110
|
+
const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
|
|
111
|
+
return end < body.length ? sourceOffset(end + 1) : undefined;
|
|
112
|
+
}});
|
|
113
|
+
let lines = 0;
|
|
114
|
+
for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
|
|
115
|
+
if (lines <= BLOCK_WINDOW) {
|
|
116
|
+
state = new parser.block.State(body, parser, env, tokens);
|
|
117
|
+
parser.block.tokenize(state, state.line, state.lineMax);
|
|
118
|
+
return report();
|
|
119
|
+
}
|
|
120
|
+
// The parser's own count of a last line that no newline ends: it does not count one of only spaces.
|
|
121
|
+
state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
|
|
122
|
+
lines += state.lineMax;
|
|
123
|
+
state.src = body;
|
|
124
|
+
const tables = new WeakMap();
|
|
125
|
+
for (const name of LINE_TABLES) { state[name].length = 0; tables.set(Object.setPrototypeOf(state[name], unheldLines), name); }
|
|
126
|
+
let frontier = 0, ended = false, betweenBlocks = false;
|
|
127
|
+
// A rule may be reading a table when it grows, so the five arrays are replaced whole; an array a rule still
|
|
128
|
+
// holds is left empty, and `answer` reads and writes the state's current one in its place.
|
|
129
|
+
const fill = line => {
|
|
130
|
+
while (!ended && line >= state.bMarks.length) {
|
|
131
|
+
const held = state.bMarks.length;
|
|
132
|
+
let until = frontier;
|
|
133
|
+
for (let more = held ? Math.max(TABLE_STEP, held - BLOCK_WINDOW) : BLOCK_WINDOW; more && until < body.length; more--) {
|
|
134
|
+
const end = body.indexOf('\n', until);
|
|
135
|
+
until = end < 0 ? body.length : end + 1;
|
|
136
|
+
}
|
|
137
|
+
const scanned = new parser.block.State(body.slice(frontier, until), parser, env, []);
|
|
138
|
+
ended = until === body.length;
|
|
139
|
+
// The scan's last entry stands for the end of its input, which is the body's end only for the last scan.
|
|
140
|
+
const added = ended ? scanned.lineMax + 1 : scanned.lineMax;
|
|
141
|
+
for (const name of LINE_TABLES) {
|
|
142
|
+
const old = state[name], next = scanned[name], offset = name === 'bMarks' || name === 'eMarks' ? frontier : 0, table = [];
|
|
143
|
+
for (let at = 0; at < held; at++) table.push(old[at]);
|
|
144
|
+
for (let at = 0; at < added; at++) table.push(next[at] + offset);
|
|
145
|
+
old.length = 0;
|
|
146
|
+
tables.set(table, name);
|
|
147
|
+
state[name] = Object.setPrototypeOf(table, unheldLines);
|
|
148
|
+
}
|
|
149
|
+
frontier = until;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const answer = (table, line, write, value) => {
|
|
153
|
+
const name = tables.get(table);
|
|
154
|
+
if (line >= state[name].length) {
|
|
155
|
+
if (betweenBlocks) throw WINDOW_END;
|
|
156
|
+
fill(line);
|
|
157
|
+
}
|
|
158
|
+
const current = state[name], held = Object.hasOwn(current, line);
|
|
159
|
+
if (!write) return held ? current[line] : undefined;
|
|
160
|
+
if (held) current[line] = value;
|
|
161
|
+
else Object.defineProperty(current, line, {value, writable: true, enumerable: true, configurable: true});
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
// The top-level loop calls this between two blocks with the line it stands on. There every token is a finished
|
|
165
|
+
// block and no rule is running, so the pass can leave the tokenizer and enter it again at that line.
|
|
166
|
+
const skipEmptyLines = state.skipEmptyLines;
|
|
167
|
+
state.skipEmptyLines = function (line) {
|
|
168
|
+
if (this.level || line !== this.line || !line) return skipEmptyLines.call(this, line);
|
|
169
|
+
if (line >= BLOCK_WINDOW) throw WINDOW_END;
|
|
170
|
+
betweenBlocks = true;
|
|
171
|
+
line = skipEmptyLines.call(this, line);
|
|
172
|
+
betweenBlocks = false;
|
|
173
|
+
return line;
|
|
174
|
+
};
|
|
175
|
+
for (;;) {
|
|
176
|
+
// Between top-level blocks the parser's loop only steps over empty lines, so a run of them is never tabled.
|
|
177
|
+
for (let at = origin; at < body.length; at++) {
|
|
178
|
+
const code = body.charCodeAt(at);
|
|
179
|
+
if (code === 10) { origin = at + 1; first++; }
|
|
180
|
+
else if (!parser.utils.isSpace(code)) break;
|
|
181
|
+
}
|
|
182
|
+
frontier = origin; ended = false; betweenBlocks = false;
|
|
183
|
+
state.line = 0; state.lineMax = lines - first;
|
|
184
|
+
const outer = unheldLine;
|
|
185
|
+
unheldLine = answer;
|
|
186
|
+
let finished = true;
|
|
187
|
+
try { parser.block.tokenize(state, 0, state.lineMax); }
|
|
188
|
+
catch (error) { if (error !== WINDOW_END) throw error; finished = false; }
|
|
189
|
+
finally { unheldLine = outer; }
|
|
190
|
+
report();
|
|
191
|
+
if (finished) return;
|
|
192
|
+
tokens.length = 0;
|
|
193
|
+
origin = state.eMarks[state.line - 1] + 1; first += state.line;
|
|
194
|
+
for (const name of LINE_TABLES) state[name].length = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
76
198
|
export function dataImage(url) {
|
|
77
199
|
if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
|
|
78
200
|
const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
|
|
@@ -94,36 +216,33 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
94
216
|
if (prior) return prior.index;
|
|
95
217
|
const definition = !bodyOffset && preparedDefinitions.find(row => row.source === source && row.factory === factory);
|
|
96
218
|
if (definition) return definition.index;
|
|
97
|
-
const parser = parserFor(factory), env = {}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
blocks.push(row);
|
|
125
|
-
if (first) assets.set(id, row);
|
|
126
|
-
}
|
|
219
|
+
const parser = parserFor(factory), env = {};
|
|
220
|
+
const assets = new Map(), blocks = [], seen = new Set(), duplicateLabels = new Set();
|
|
221
|
+
eachBlockWindow(source, bodyOffset, parser, env, ({tokens, lineStart}) => {
|
|
222
|
+
for (const token of tokens) {
|
|
223
|
+
if (token.type !== 'reference_definition' || !token.map) continue;
|
|
224
|
+
const id = token.meta.label, definition = token.meta.mdImageDefinition, first = !seen.has(id);
|
|
225
|
+
if (!first) duplicateLabels.add(id);
|
|
226
|
+
seen.add(id);
|
|
227
|
+
const info = dataImage(definition?.href);
|
|
228
|
+
if (!info) continue;
|
|
229
|
+
const start = lineStart(token.map[0]), stop = lineStart(token.map[1]) ?? source.length;
|
|
230
|
+
let end = stop;
|
|
231
|
+
if (source[end - 1] === '\n') end--;
|
|
232
|
+
if (source[end - 1] === '\r') end--;
|
|
233
|
+
// Repeated definitions are source, not additional image owners.
|
|
234
|
+
const raw = source.slice(start, end), found = raw.indexOf(definition.href);
|
|
235
|
+
const repeated = found >= 0 ? raw.indexOf(definition.href, found + 1) : -1;
|
|
236
|
+
const labelContainsUrl = definition.href.length <= id.length && id.includes(parser.utils.normalizeReference(definition.href));
|
|
237
|
+
const urlStart = found >= 0 && repeated < 0 && !labelContainsUrl ? start + found : null;
|
|
238
|
+
const row = {id, label: id, source: raw, url: definition.href, title: definition.title || '', start, end,
|
|
239
|
+
...info, urlStart, urlEnd: urlStart == null ? null : urlStart + definition.href.length,
|
|
240
|
+
payloadStart: urlStart == null ? null : urlStart + info.payloadStart, payloadEnd: urlStart == null ? null : urlStart + definition.href.length, status: 'unverified', active: first, topLevel: token.level === 0};
|
|
241
|
+
blocks.push(row);
|
|
242
|
+
if (first) assets.set(id, row);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
const references = env.references || Object.create(null);
|
|
127
246
|
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
128
247
|
let appendixStart = source.length;
|
|
129
248
|
for (let index = blocks.length - 1; index >= 0; index--) {
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -103,14 +103,16 @@ export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(v
|
|
|
103
103
|
export function textStyle(value, {portable = false, pointSize, indentOffset = 0} = {}) {
|
|
104
104
|
if (!validLayout(value)) return '';
|
|
105
105
|
const property = {first: 'text-indent', indent: 'margin-inline-start'};
|
|
106
|
-
|
|
106
|
+
const css = (portable ? ['first', 'indent'] : ['first', 'indent', 'marker'])
|
|
107
107
|
.filter(key => has(value, key) || key === 'indent' && indentOffset).map(key => {
|
|
108
108
|
const level = (value[key] || 0) + (key === 'indent' ? indentOffset : 0);
|
|
109
109
|
const measure = decimal(Number((level * 2).toFixed(6)));
|
|
110
110
|
return (portable ? property[key] : '--md-' + key) + ':' + (portable ?
|
|
111
111
|
pointSize ? decimal(Math.round(level * pointSize * 40) / 20) + 'pt' : measure + 'em' :
|
|
112
112
|
'calc(' + measure + ' * var(--md-text-body))');
|
|
113
|
-
})
|
|
113
|
+
});
|
|
114
|
+
if (portable && value.hyphens === 'auto') css.push('hyphens:auto');
|
|
115
|
+
return css.join(';');
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// Raw HTML tables carry ordinary paragraph CSS. Read the browser's parsed declarations on the same
|
|
@@ -127,6 +129,7 @@ export function textLayout(style, {listMarker = false, pointSize, offset = 0} =
|
|
|
127
129
|
// rather than each intermediate declaration, is the measurement the writer must keep.
|
|
128
130
|
if (Number.isFinite(level) && level) value[key] = level;
|
|
129
131
|
}
|
|
132
|
+
if (!listMarker && style?.getPropertyValue('hyphens').trim() === 'auto') value.hyphens = 'auto';
|
|
130
133
|
return value;
|
|
131
134
|
}
|
|
132
135
|
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The one owner of
|
|
2
|
+
// The one owner of inline typography, authored colour/font comments, ink and page breaks.
|
|
3
|
+
import {markdownSourcePositions} from './md-source.mjs';
|
|
3
4
|
|
|
4
5
|
// The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
|
|
5
6
|
const inkReader = () => {
|
|
@@ -84,7 +85,7 @@ export function formatColorOpen(hex) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export function formatColorRun(hex, content) {
|
|
87
|
-
return
|
|
88
|
+
return typography.wrap({color: hex}, content);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
// Relocated into the parse Worker by .toString() (engine.js _buildParseWorkerSource): read only parameters, TEXT_COLOR_NAMES and COLOR_CLOSE.
|
|
@@ -165,7 +166,7 @@ export function formatFontOpen(family) {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export function formatFontRun(family, content) {
|
|
168
|
-
return
|
|
169
|
+
return typography.wrap({family}, content);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
export function matchFontOpen(text) {
|
|
@@ -209,6 +210,220 @@ export function hasFontMarker(source) {
|
|
|
209
210
|
return scanFontMarkers(source).length > 0;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
// One typed interpretation for source, DOM and native-file adapters. The worker carries this
|
|
214
|
+
// exact factory and its explicit grammar inputs; no DOM, CSS evaluator or resource loader.
|
|
215
|
+
export function createTypography({normalizeFontFamily, fontFamilyCss, parseColorOpen, parseFontOpen, FONT_STACKS}) {
|
|
216
|
+
const LIMIT = 4096, DEPTH = 64;
|
|
217
|
+
const generics = {serif: 'serif', 'sans-serif': 'sans', monospace: 'mono', cursive: 'hand',
|
|
218
|
+
fantasy: 'fantasy', 'system-ui': 'system', 'ui-serif': 'ui-serif', 'ui-sans-serif': 'ui-sans-serif',
|
|
219
|
+
'ui-monospace': 'ui-monospace', 'ui-rounded': 'ui-rounded', emoji: 'emoji', math: 'math', fangsong: 'fangsong'};
|
|
220
|
+
const escape = value => String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
221
|
+
function size(value) {
|
|
222
|
+
if (typeof value !== 'string' || value.length > 48) return null;
|
|
223
|
+
const match = /^(\d+(?:\.\d{0,12})?|\.\d{1,12})\s*(pt|px)$/i.exec(value.trim());
|
|
224
|
+
if (!match) return null;
|
|
225
|
+
const number = Number(match[1]), unit = match[2].toLowerCase(), points = number * (unit === 'px' ? .75 : 1);
|
|
226
|
+
if (!(points > 0 && points <= 1638)) return null;
|
|
227
|
+
const [integer, fraction = ''] = match[1].split('.'), tail = fraction.replace(/0+$/, '');
|
|
228
|
+
return Object.freeze({css: (integer.replace(/^0+/, '') || '0') + (tail ? '.' + tail : '') + unit,
|
|
229
|
+
unit, value: number, points, halfPoints: points * 2});
|
|
230
|
+
}
|
|
231
|
+
function color(value) {
|
|
232
|
+
if (typeof value !== 'string') return null;
|
|
233
|
+
if (/^#[0-9a-f]{6}$/i.test(value.trim())) return value.trim().toLowerCase();
|
|
234
|
+
// CSSOM serializes admitted hex to opaque rgb(). Native HTML import uses this same boundary.
|
|
235
|
+
const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*1\s*)?\)$/i.exec(value.trim());
|
|
236
|
+
return rgb && rgb.slice(1).every(channel => Number(channel) <= 255)
|
|
237
|
+
? '#' + rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, '0')).join('') : null;
|
|
238
|
+
}
|
|
239
|
+
function familyCss(value) {
|
|
240
|
+
if (typeof value !== 'string' || value.length > 2048) return null;
|
|
241
|
+
const css = value.trim();
|
|
242
|
+
const stackKey = text => text.replace(/'/g, '"').replace(/"([\w-]+)"/g, '$1').replace(/\s*,\s*/g, ',');
|
|
243
|
+
for (const [name] of Object.entries(FONT_STACKS)) if ((!/^["']/.test(css) || css.includes(',')) && stackKey(css) === stackKey(fontFamilyCss(name)))
|
|
244
|
+
return Object.freeze({name, css: fontFamilyCss(name), custom: false});
|
|
245
|
+
if (Object.hasOwn(generics, css.toLowerCase())) return Object.freeze({name: generics[css.toLowerCase()], css: css.toLowerCase(), custom: false});
|
|
246
|
+
let name = css;
|
|
247
|
+
if (css[0] === '"' || css[0] === "'") {
|
|
248
|
+
if (css.at(-1) !== css[0]) return null;
|
|
249
|
+
name = '';
|
|
250
|
+
for (let at = 1; at < css.length - 1; at++) {
|
|
251
|
+
if (css[at] === css[0] || /[\r\n\f]/.test(css[at])) return null;
|
|
252
|
+
if (css[at] !== '\\') { name += css[at]; continue; }
|
|
253
|
+
const escaped = /^\\(?:([0-9a-f]{1,6})(?:\r\n|[ \t\r\n\f])?|([^\r\n\f]))/i.exec(css.slice(at));
|
|
254
|
+
if (!escaped || at + escaped[0].length > css.length - 1) return null;
|
|
255
|
+
const code = escaped[1] ? parseInt(escaped[1], 16) : 0;
|
|
256
|
+
name += escaped[1] ? code && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff) ? String.fromCodePoint(code) : '\ufffd' : escaped[2];
|
|
257
|
+
at += escaped[0].length - 1;
|
|
258
|
+
}
|
|
259
|
+
} else if (!/^[\p{L}_-][\p{L}\p{N}_ -]*$/u.test(css) || /^(?:inherit|initial|unset|revert|revert-layer)$/i.test(css)) return null;
|
|
260
|
+
name = normalizeFontFamily(name);
|
|
261
|
+
// These five names are the existing family-choice aliases, not custom font identities.
|
|
262
|
+
if (name && Object.hasOwn(FONT_STACKS, name.toLowerCase())) return null;
|
|
263
|
+
return name ? Object.freeze({name, css: '"' + name.replace(/[\\"]/g, '\\$&') + '"', custom: true}) : null;
|
|
264
|
+
}
|
|
265
|
+
function family(value) {
|
|
266
|
+
if (value && typeof value === 'object') {
|
|
267
|
+
const parsed = familyCss(value.css);
|
|
268
|
+
return parsed && parsed.name === value.name && parsed.custom === value.custom ? parsed : null;
|
|
269
|
+
}
|
|
270
|
+
const name = normalizeFontFamily(value);
|
|
271
|
+
return name ? familyCss(fontFamilyCss(name)) : null;
|
|
272
|
+
}
|
|
273
|
+
function normalize(value, removals = false) {
|
|
274
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Typography requires size, color or family.');
|
|
275
|
+
const result = {};
|
|
276
|
+
for (const key of Object.keys(value)) {
|
|
277
|
+
if (!['size', 'color', 'family'].includes(key)) throw new TypeError('Unknown typography property: ' + key);
|
|
278
|
+
if (value[key] === null && removals) { result[key] = null; continue; }
|
|
279
|
+
const parsed = key === 'size' ? size(value[key])?.css : key === 'color' ? color(value[key]) : family(value[key]);
|
|
280
|
+
if (!parsed) throw new TypeError('Invalid typography ' + key + '.');
|
|
281
|
+
result[key] = parsed;
|
|
282
|
+
}
|
|
283
|
+
return Object.freeze(result);
|
|
284
|
+
}
|
|
285
|
+
// Rendering drops unsupported declarations; rich editing admits only wholly owned
|
|
286
|
+
// spans so unrelated HTML and rejected attributes keep their exact-source editor.
|
|
287
|
+
function parseStyle(value, editable = false) {
|
|
288
|
+
if (typeof value !== 'string' || value.length > LIMIT) return editable ? null : Object.freeze({});
|
|
289
|
+
const parts = []; let quote = '', start = 0;
|
|
290
|
+
for (let at = 0; at <= value.length; at++) {
|
|
291
|
+
const ch = value[at];
|
|
292
|
+
if (quote) { if (ch === '\\') at++; else if (ch === quote) quote = ''; }
|
|
293
|
+
else if (ch === '"' || ch === "'") quote = ch;
|
|
294
|
+
else if (ch === ';' || at === value.length) { parts.push(value.slice(start, at)); start = at + 1; }
|
|
295
|
+
}
|
|
296
|
+
if (quote || parts.length > 32) return editable ? null : Object.freeze({});
|
|
297
|
+
const result = {};
|
|
298
|
+
for (const part of parts) {
|
|
299
|
+
if (!part.trim()) continue;
|
|
300
|
+
const colon = part.indexOf(':'); if (colon < 0) { if (editable) return null; continue; }
|
|
301
|
+
const key = part.slice(0, colon).trim().toLowerCase(), raw = part.slice(colon + 1).trim();
|
|
302
|
+
const parsed = key === 'font-size' ? size(raw)?.css : key === 'color' ? color(raw) : key === 'font-family' ? familyCss(raw) : null;
|
|
303
|
+
if (parsed) result[key === 'font-size' ? 'size' : key === 'font-family' ? 'family' : 'color'] = parsed;
|
|
304
|
+
else if (editable) return null;
|
|
305
|
+
}
|
|
306
|
+
return Object.freeze(result);
|
|
307
|
+
}
|
|
308
|
+
function style(value) {
|
|
309
|
+
const held = normalize(value), parts = [];
|
|
310
|
+
if (held.size) parts.push('font-size:' + held.size);
|
|
311
|
+
if (held.color) parts.push('color:' + held.color);
|
|
312
|
+
if (held.family) parts.push('font-family:' + held.family.css);
|
|
313
|
+
return parts.join(';');
|
|
314
|
+
}
|
|
315
|
+
const open = value => { const css = style(value); return css ? '<span style="' + escape(css) + '">' : ''; };
|
|
316
|
+
const wrap = (value, content) => {
|
|
317
|
+
const start = open(value);
|
|
318
|
+
if (!start || !content) return content;
|
|
319
|
+
// Importers can supply a styled break by itself, or before/after words. Keep
|
|
320
|
+
// those exact line-edge bytes outside the new span so its opener has words.
|
|
321
|
+
const prefix = /^(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+/.exec(content)?.[0] || '';
|
|
322
|
+
let body = content.slice(prefix.length);
|
|
323
|
+
const suffix = /(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+[ \t]*$/.exec(body)?.[0] || '';
|
|
324
|
+
body = body.slice(0, body.length - suffix.length);
|
|
325
|
+
if (!body) return content;
|
|
326
|
+
// Soft/hard line breaks can belong to one Markdown paragraph. A blank line or
|
|
327
|
+
// an opener on a line of its own would instead change the block grammar.
|
|
328
|
+
if (/^\s*\n|\n[ \t]*\n/.test(body.replace(/\r\n?/g, '\n')))
|
|
329
|
+
throw new TypeError('Typography wraps one inline paragraph at a time.');
|
|
330
|
+
return prefix + start + body + '</span>' + suffix;
|
|
331
|
+
};
|
|
332
|
+
function parseOpen(source, decode = value => value, editable = false) {
|
|
333
|
+
if (typeof source !== 'string' || source.length > LIMIT + 1024 || !/^<span(?=[\s>])/i.test(source) || !source.endsWith('>') || /\/\s*>$/.test(source)) return null;
|
|
334
|
+
const attrs = /\s+([^\s=<>/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s=<>`"']+)))?/y;
|
|
335
|
+
let at = 5, css = null;
|
|
336
|
+
while (at < source.length - 1) {
|
|
337
|
+
if (/^\s*>$/.test(source.slice(at))) break;
|
|
338
|
+
attrs.lastIndex = at; const match = attrs.exec(source);
|
|
339
|
+
if (!match) return null;
|
|
340
|
+
at = attrs.lastIndex;
|
|
341
|
+
if (match[1].toLowerCase() !== 'style') { if (editable) return null; continue; }
|
|
342
|
+
if (css !== null) return null;
|
|
343
|
+
css = decode(match[2] ?? match[3] ?? match[4] ?? '');
|
|
344
|
+
}
|
|
345
|
+
const facts = parseStyle(css, editable);
|
|
346
|
+
return facts && Object.keys(facts).length ? facts : null;
|
|
347
|
+
}
|
|
348
|
+
function nodeStyle(node) {
|
|
349
|
+
const result = {...parseStyle(node?.getAttribute?.('style') || '')};
|
|
350
|
+
const authoredSize = size(node?.getAttribute?.('data-md-size'));
|
|
351
|
+
const authoredColor = color(node?.getAttribute?.('data-md-color'));
|
|
352
|
+
const authoredFamily = normalizeFontFamily(node?.getAttribute?.('data-md-font'));
|
|
353
|
+
if (authoredSize) result.size = authoredSize.css;
|
|
354
|
+
if (authoredColor) result.color = authoredColor;
|
|
355
|
+
if (authoredFamily && !result.family) result.family = family(authoredFamily);
|
|
356
|
+
return Object.freeze(result);
|
|
357
|
+
}
|
|
358
|
+
function runs(root) {
|
|
359
|
+
let text = ''; const paint = [], leaves = [];
|
|
360
|
+
const visit = (node, inherited, blocked) => {
|
|
361
|
+
if (node.nodeType === 3) {
|
|
362
|
+
const value = String(node.nodeValue || '').replace(/\u2060/g, '\n'), start = text.length;
|
|
363
|
+
text += value;
|
|
364
|
+
for (let index = 0; index < value.length; index++) paint.push(inherited);
|
|
365
|
+
if (value) leaves.push({start, end: text.length, blocked});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (node.nodeType !== 1 && node !== root) return;
|
|
369
|
+
const own = nodeStyle(node), next = {...inherited, ...own};
|
|
370
|
+
const literal = blocked || /^(?:CODE|PRE|KBD|SAMP|MATH)$/.test(node.tagName) ||
|
|
371
|
+
node.classList?.contains('math-rendered') || node.classList?.contains('math-placeholder');
|
|
372
|
+
for (const child of node.childNodes || []) visit(child, next, !!literal);
|
|
373
|
+
};
|
|
374
|
+
visit(root, {}, false);
|
|
375
|
+
return {text, paint, leaves};
|
|
376
|
+
}
|
|
377
|
+
function install(md) {
|
|
378
|
+
if (md.__rapierTypography) return md;
|
|
379
|
+
md.__rapierTypography = true;
|
|
380
|
+
md.core.ruler.after('linkify', 'rapier-typography', state => {
|
|
381
|
+
for (const block of state.tokens) {
|
|
382
|
+
if (block.type !== 'inline' || !block.children) continue;
|
|
383
|
+
const stack = []; let depth = 0, overflow = 0;
|
|
384
|
+
for (const token of block.children) {
|
|
385
|
+
if (token.type !== 'html_inline') { depth += token.nesting || 0; continue; }
|
|
386
|
+
const source = token.content, htmlClose = /^<\/span\s*>$/i.test(source);
|
|
387
|
+
const kind = htmlClose || /^<span(?=[\s>])/i.test(source) ? 'span'
|
|
388
|
+
: source === '<!--/c-->' || parseColorOpen(source) ? 'color'
|
|
389
|
+
: source === '<!--/f-->' || parseFontOpen(source) ? 'font' : '';
|
|
390
|
+
if (!kind) continue;
|
|
391
|
+
const close = htmlClose || source === '<!--/c-->' || source === '<!--/f-->';
|
|
392
|
+
if (overflow) { overflow += close ? -1 : 1; continue; }
|
|
393
|
+
if (!close) {
|
|
394
|
+
// Authored comment marks retain their original first-opener rule. Ordinary
|
|
395
|
+
// canonical spans nest; a repeated old opener remains an inert stray.
|
|
396
|
+
if (kind !== 'span' && stack.some(held => held.kind === kind)) continue;
|
|
397
|
+
if (stack.length === DEPTH) { overflow = 1; continue; }
|
|
398
|
+
const facts = kind === 'span' ? parseOpen(source, value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\')))
|
|
399
|
+
: kind === 'color' ? normalize({color: parseColorOpen(source)}) : normalize({family: parseFontOpen(source)});
|
|
400
|
+
stack.push({token, kind, facts, depth, source}); continue;
|
|
401
|
+
}
|
|
402
|
+
const held = stack.at(-1);
|
|
403
|
+
if (!held || held.kind !== kind) continue;
|
|
404
|
+
stack.pop();
|
|
405
|
+
if (!held.facts || depth !== held.depth) continue;
|
|
406
|
+
const type = kind === 'span' ? 'typography' : kind, opener = held.token;
|
|
407
|
+
opener.type = 'rapier_' + type + '_open'; opener.tag = 'span'; opener.nesting = 1; opener.content = '';
|
|
408
|
+
(opener.meta ||= {}).typography = Object.freeze({style: held.facts, open: held.source, close: source});
|
|
409
|
+
opener.attrSet('style', style(held.facts));
|
|
410
|
+
if (held.facts.color) opener.attrSet('data-md-color', held.facts.color);
|
|
411
|
+
if (held.facts.size) opener.attrSet('data-md-size', held.facts.size);
|
|
412
|
+
if (held.facts.family) opener.attrSet('data-md-font', held.facts.family.name);
|
|
413
|
+
if (kind === 'font') { opener.attrSet('data-md-font-open', encodeURIComponent(held.source)); opener.meta.mdFont = {source: held.source, family: held.facts.family.name}; }
|
|
414
|
+
token.type = 'rapier_' + type + '_close'; token.tag = 'span'; token.nesting = -1; token.content = '';
|
|
415
|
+
(token.meta ||= {}).typography = opener.meta.typography;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return md;
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze({size, color, family, familyCss, normalize, parseStyle, style, open, wrap, parseOpen, nodeStyle, runs, install});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const typography = createTypography({normalizeFontFamily, fontFamilyCss, parseColorOpen, parseFontOpen, FONT_STACKS});
|
|
425
|
+
export function typographySourceRanges(source, parser) { return markdownSourcePositions(source, parser).typography; }
|
|
426
|
+
|
|
212
427
|
const INK_CLOSE = '<!--/ink-->';
|
|
213
428
|
export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN };
|
|
214
429
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {splitOpeningFrontmatter} from './frontmatter.mjs';
|
|
3
|
+
|
|
4
|
+
// The standard parser owns literal regions and comment boundaries. Offsets are original UTF-16,
|
|
5
|
+
// including BOM/CRLF and list/quote prefixes. A private parser view observes rules; it changes no
|
|
6
|
+
// live rule or token. Call once per source, then ask isText(position) or isComment(start, end).
|
|
7
|
+
export function markdownSourcePositions(source, parser) {
|
|
8
|
+
if (typeof source !== 'string' || !parser) throw new TypeError('Source positions require text and its Markdown parser');
|
|
9
|
+
const {body, bodyOffset} = splitOpeningFrontmatter(source), lines = [bodyOffset];
|
|
10
|
+
for (const row of body.matchAll(/\r\n?|\n/g)) lines.push(bodyOffset + row.index + row[0].length);
|
|
11
|
+
const lineAt = line => lines[line] ?? source.length;
|
|
12
|
+
const literal = [], comments = new Map(), observed = new WeakMap(), typography = [], openedStyles = new Map();
|
|
13
|
+
const add = (start, end) => { if (end > start) literal.push({start, end}); };
|
|
14
|
+
if (bodyOffset) add(0, bodyOffset);
|
|
15
|
+
const owner = Object.create(parser), inline = Object.create(parser.inline), ruler = Object.create(parser.inline.ruler);
|
|
16
|
+
owner.inline = inline; inline.ruler = ruler;
|
|
17
|
+
ruler.getRules = chain => parser.inline.ruler.getRules(chain).map(rule => (state, silent) => {
|
|
18
|
+
const start = state.pos, first = state.tokens.length;
|
|
19
|
+
const matched = rule(state, silent), token = state.tokens.at(-1);
|
|
20
|
+
// A pending text token may flush before this token. Never overwrite a nested rule's span
|
|
21
|
+
// with its enclosing link's range.
|
|
22
|
+
if (matched && !silent && state.tokens.length > first && !observed.has(token) &&
|
|
23
|
+
['code_inline', 'html_inline', 'image', 'footnote_ref'].includes(token?.type)) {
|
|
24
|
+
observed.set(token, {start, end: state.pos, source: state.src,
|
|
25
|
+
comment: token.type === 'html_inline' && token.content.startsWith('<!--'),
|
|
26
|
+
footnote: token.type === 'footnote_ref' && state.src.startsWith('^[', start)
|
|
27
|
+
? state.env.footnotes?.list?.[token.meta.id] : null});
|
|
28
|
+
}
|
|
29
|
+
return matched;
|
|
30
|
+
});
|
|
31
|
+
const tokens = owner.parse(body, {});
|
|
32
|
+
const readInline = (children, content, rawOffset) => {
|
|
33
|
+
for (const child of children || []) {
|
|
34
|
+
const span = observed.get(child);
|
|
35
|
+
if (!span || span.source !== content) continue;
|
|
36
|
+
// Inline footnotes are moved to a generated footer without line maps. Their parser-owned
|
|
37
|
+
// child tokens still stand inside the original ^[...] carrier, not in that footer.
|
|
38
|
+
if (span.footnote?.tokens) {
|
|
39
|
+
readInline(span.footnote.tokens, span.footnote.content, pos => rawOffset(span.start + 2 + pos));
|
|
40
|
+
} else if (child.type !== 'footnote_ref') {
|
|
41
|
+
const start = rawOffset(span.start), end = rawOffset(span.end);
|
|
42
|
+
const style = child.meta?.typography;
|
|
43
|
+
if (style && child.nesting === 1) openedStyles.set(style, {start, innerStart: end, style: style.style});
|
|
44
|
+
else if (style && child.nesting === -1 && openedStyles.has(style)) {
|
|
45
|
+
typography.push({...openedStyles.get(style), innerEnd: start, end});
|
|
46
|
+
openedStyles.delete(style);
|
|
47
|
+
}
|
|
48
|
+
if (span.comment) comments.set(start, end);
|
|
49
|
+
else add(start, end);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
let table = null;
|
|
54
|
+
for (const token of tokens) {
|
|
55
|
+
if (token.type === 'tr_open' && token.map) {
|
|
56
|
+
// GFM unescapes a pipe before parsing a cell. Preserve that one-character projection and
|
|
57
|
+
// consume cells in order, so repeated cells and backticks across a separator never alias.
|
|
58
|
+
const start = lineAt(token.map[0]), end = lineAt(token.map[0] + 1), offsets = [];
|
|
59
|
+
let text = '';
|
|
60
|
+
for (let at = start; at < end; at++) {
|
|
61
|
+
if (source[at] === '\\' && source[at + 1] === '|') at++;
|
|
62
|
+
offsets.push(at); text += source[at] === '\0' ? '\ufffd' : source[at];
|
|
63
|
+
}
|
|
64
|
+
offsets.push(end); table = {text, offsets, at: 0};
|
|
65
|
+
}
|
|
66
|
+
if (token.type === 'tr_close') table = null;
|
|
67
|
+
if (token.map && ['fence', 'code_block', 'html_block', 'reference_definition'].includes(token.type)) {
|
|
68
|
+
const start = lineAt(token.map[0]), end = lineAt(token.map[1]);
|
|
69
|
+
add(start, end);
|
|
70
|
+
// A comment-carrier block can contain several adjacent comments. Keep each carrier,
|
|
71
|
+
// including a closer surviving damage to its opener. Never promote nested comments or
|
|
72
|
+
// comment-shaped text in a raw tagged HTML block (pre, script, div, ...).
|
|
73
|
+
if (token.type === 'html_block' && /^\s*<!--/.test(token.content)) {
|
|
74
|
+
let at = source.indexOf('<!--', start);
|
|
75
|
+
while (at >= start && at < end) {
|
|
76
|
+
const close = source.indexOf('-->', at + 4);
|
|
77
|
+
const stop = close >= 0 && close + 3 <= end ? close + 3 : end;
|
|
78
|
+
comments.set(at, stop);
|
|
79
|
+
at = source.indexOf('<!--', stop);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (token.type !== 'inline' || !token.content) continue;
|
|
85
|
+
let rawOffset;
|
|
86
|
+
if (token.map) {
|
|
87
|
+
const offsets = [], starts = [], rows = token.content.split('\n');
|
|
88
|
+
let local = 0;
|
|
89
|
+
for (let i = 0; i < rows.length; i++) {
|
|
90
|
+
const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1);
|
|
91
|
+
const at = source.slice(start, end).replace(/\0/g, '\ufffd').indexOf(rows[i]);
|
|
92
|
+
if (at < 0) throw new Error('Markdown inline source mapping failed');
|
|
93
|
+
starts.push(local); offsets.push(start + at); local += rows[i].length + 1;
|
|
94
|
+
}
|
|
95
|
+
rawOffset = pos => {
|
|
96
|
+
let row = starts.length - 1;
|
|
97
|
+
while (row > 0 && starts[row] > pos) row--;
|
|
98
|
+
return offsets[row] + pos - starts[row];
|
|
99
|
+
};
|
|
100
|
+
} else if (table) {
|
|
101
|
+
const at = table.text.indexOf(token.content, table.at);
|
|
102
|
+
if (at < 0) throw new Error('Markdown table source mapping failed');
|
|
103
|
+
table.at = at + token.content.length;
|
|
104
|
+
rawOffset = pos => table.offsets[at + pos];
|
|
105
|
+
} else continue;
|
|
106
|
+
readInline(token.children, token.content, rawOffset);
|
|
107
|
+
}
|
|
108
|
+
literal.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
109
|
+
const merged = [];
|
|
110
|
+
for (const range of literal) {
|
|
111
|
+
const last = merged.at(-1);
|
|
112
|
+
if (last && range.start <= last.end) last.end = Math.max(last.end, range.end);
|
|
113
|
+
else merged.push({...range});
|
|
114
|
+
}
|
|
115
|
+
const isText = position => {
|
|
116
|
+
if (comments.has(position)) return false;
|
|
117
|
+
let low = 0, high = merged.length;
|
|
118
|
+
while (low < high) { const mid = (low + high) >> 1; if (merged[mid].end <= position) low = mid + 1; else high = mid; }
|
|
119
|
+
return low < merged.length && merged[low].start <= position;
|
|
120
|
+
};
|
|
121
|
+
return {isText, isComment: (start, end) => comments.get(start) === end, typography: typography.sort((a, b) => a.start - b.start)};
|
|
122
|
+
}
|