@astryxdesign/core 0.4.7-canary.e93acd6 → 0.4.7-canary.ecaf779
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +1 -30
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +23 -138
- package/package.json +2 -2
- package/src/Markdown/index.ts +0 -1
- package/src/Markdown/parser.test.ts +2 -133
- package/src/Markdown/parser.ts +22 -203
package/dist/Markdown/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
export { Markdown } from './Markdown';
|
|
7
7
|
export type { MarkdownProps, MarkdownSource, MarkdownComponents, MarkdownInlinePlugin, } from './Markdown';
|
|
8
8
|
export { parseMarkdown, parseMarkdownIncremental, createIncrementalState, parseInline, } from './parser';
|
|
9
|
-
export type { BlockNode, InlineNode,
|
|
9
|
+
export type { BlockNode, InlineNode, ListItemNode, TableCellNode, TableAlignment, IncrementalState as IncrementalParseState, } from './parser';
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Markdown/index.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,sBAAsB,EACtB,WAAW,GACZ,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Markdown/index.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,sBAAsB,EACtB,WAAW,GACZ,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,UAAU,EACV,YAAY,EACZ,aAAa,EACb,cAAc,EACd,gBAAgB,IAAI,qBAAqB,GAC1C,MAAM,UAAU,CAAC"}
|
|
@@ -35,14 +35,7 @@ export type InlineNode = {
|
|
|
35
35
|
} | {
|
|
36
36
|
type: 'break';
|
|
37
37
|
};
|
|
38
|
-
export type BlockNode =
|
|
39
|
-
/**
|
|
40
|
-
* Where this block came from in the source, when parsed with the
|
|
41
|
-
* `sourceRanges` option. Top-level blocks only.
|
|
42
|
-
*/
|
|
43
|
-
range?: SourceRange;
|
|
44
|
-
};
|
|
45
|
-
type BlockNodeKind = {
|
|
38
|
+
export type BlockNode = {
|
|
46
39
|
type: 'heading';
|
|
47
40
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
48
41
|
children: InlineNode[];
|
|
@@ -76,11 +69,6 @@ type BlockNodeKind = {
|
|
|
76
69
|
src: string;
|
|
77
70
|
alt: string;
|
|
78
71
|
};
|
|
79
|
-
/**
|
|
80
|
-
* Character offsets `[start, end)` of a block in the source string handed to
|
|
81
|
-
* `parseMarkdown`. `end` excludes the block's trailing blank lines.
|
|
82
|
-
*/
|
|
83
|
-
export type SourceRange = readonly [start: number, end: number];
|
|
84
72
|
export type ListItemNode = {
|
|
85
73
|
checked?: boolean;
|
|
86
74
|
children: BlockNode[];
|
|
@@ -112,16 +100,6 @@ export type ParseOptions = {
|
|
|
112
100
|
* suffix is not rejected (Astryx accepts any plausible TLD shape).
|
|
113
101
|
*/
|
|
114
102
|
autolink?: 'gfm';
|
|
115
|
-
/**
|
|
116
|
-
* When true, every top-level block carries a `range` — the offsets it
|
|
117
|
-
* occupies in the string passed in. Lets a consumer that still holds the
|
|
118
|
-
* source slice the original markdown for a block instead of reconstructing
|
|
119
|
-
* it from the parsed node (or from the rendered DOM). Off by default: the
|
|
120
|
-
* field is absent unless asked for, so nothing that compares nodes changes.
|
|
121
|
-
*
|
|
122
|
-
* Blocks nested inside a list item or a blockquote do not carry one.
|
|
123
|
-
*/
|
|
124
|
-
sourceRanges?: boolean;
|
|
125
103
|
};
|
|
126
104
|
export declare function parseInline(text: string, sourceIds?: ReadonlySet<string>): InlineNode[];
|
|
127
105
|
export declare function parseInline(text: string, options: ParseOptions): InlineNode[];
|
|
@@ -139,12 +117,6 @@ export interface IncrementalState {
|
|
|
139
117
|
* with newly-arriving content.
|
|
140
118
|
*/
|
|
141
119
|
autolink?: 'gfm';
|
|
142
|
-
/**
|
|
143
|
-
* The `sourceRanges` option the cached `settledBlocks` were parsed with.
|
|
144
|
-
* Flipping it invalidates them the same way `autolink` does: they either
|
|
145
|
-
* lack the ranges the caller now asks for, or carry ones it did not.
|
|
146
|
-
*/
|
|
147
|
-
sourceRanges?: boolean;
|
|
148
120
|
/**
|
|
149
121
|
* Signature of the link reference definitions the cached `settledBlocks`
|
|
150
122
|
* were parsed with. Definitions are document-global and typically arrive
|
|
@@ -171,5 +143,4 @@ export declare function slugify(value: string): string;
|
|
|
171
143
|
* document shares a single numbering sequence.
|
|
172
144
|
*/
|
|
173
145
|
export declare function uniqueSlug(baseSlug: string, counts: Map<string, number>): string;
|
|
174
|
-
export {};
|
|
175
146
|
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/Markdown/parser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAMH,MAAM,MAAM,UAAU,GAClB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAC/B;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;CAAC,GACtC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;CAAC,GACxC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;CAAC,GAC/C;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAC/B;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;CAAC,GACpD;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,GACzC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,GACpC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAC,CAAC;AAEpB,MAAM,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/Markdown/parser.ts"],"names":[],"mappings":"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"}
|
package/dist/Markdown/parser.js
CHANGED
|
@@ -13,11 +13,6 @@
|
|
|
13
13
|
// Types
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Character offsets `[start, end)` of a block in the source string handed to
|
|
18
|
-
* `parseMarkdown`. `end` excludes the block's trailing blank lines.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
16
|
// ---------------------------------------------------------------------------
|
|
22
17
|
// Parse options
|
|
23
18
|
// ---------------------------------------------------------------------------
|
|
@@ -52,8 +47,7 @@ function resolveOptions(arg) {
|
|
|
52
47
|
const opts = arg;
|
|
53
48
|
return {
|
|
54
49
|
sourceIds: opts.sourceIds,
|
|
55
|
-
autolink: opts.autolink
|
|
56
|
-
sourceRanges: opts.sourceRanges
|
|
50
|
+
autolink: opts.autolink
|
|
57
51
|
};
|
|
58
52
|
}
|
|
59
53
|
|
|
@@ -174,17 +168,10 @@ function extractLinkDefinitions(input) {
|
|
|
174
168
|
cleaned: input
|
|
175
169
|
};
|
|
176
170
|
}
|
|
177
|
-
const
|
|
178
|
-
for (let index = 0; index < lines.length; index++) {
|
|
179
|
-
if (keep[index]) {
|
|
180
|
-
lineMap.push(index);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const cleaned = lineMap.map(index => lines[index]).join('\n');
|
|
171
|
+
const cleaned = lines.filter((_, index) => keep[index]).join('\n');
|
|
184
172
|
return {
|
|
185
173
|
defs,
|
|
186
|
-
cleaned
|
|
187
|
-
lineMap
|
|
174
|
+
cleaned
|
|
188
175
|
};
|
|
189
176
|
}
|
|
190
177
|
|
|
@@ -978,19 +965,6 @@ function isTableSeparator(line) {
|
|
|
978
965
|
return nonEmpty.length > 0 && nonEmpty.every(cell => /^:?-+:?$/.test(cell));
|
|
979
966
|
}
|
|
980
967
|
|
|
981
|
-
/**
|
|
982
|
-
* The same options for content parsed out of an enclosing block. Ranges are a
|
|
983
|
-
* top-level contract: a list item's or a blockquote's children are parsed from
|
|
984
|
-
* text the caller reassembled (markers and `>` prefixes stripped), so an
|
|
985
|
-
* offset into it would not address the document.
|
|
986
|
-
*/
|
|
987
|
-
function nested(opts) {
|
|
988
|
-
return opts.sourceRanges ? {
|
|
989
|
-
...opts,
|
|
990
|
-
sourceRanges: false
|
|
991
|
-
} : opts;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
968
|
/**
|
|
995
969
|
* Returns true when a line could start a new block — used to stop paragraph
|
|
996
970
|
* continuation. Every regex here uses bounded or single-class quantifiers
|
|
@@ -1121,7 +1095,7 @@ function parseList(lines, startIndex, ordered, opts) {
|
|
|
1121
1095
|
}
|
|
1122
1096
|
items.push({
|
|
1123
1097
|
checked,
|
|
1124
|
-
children: parseMarkdownImpl(itemText,
|
|
1098
|
+
children: parseMarkdownImpl(itemText, opts)
|
|
1125
1099
|
});
|
|
1126
1100
|
|
|
1127
1101
|
// CommonMark loose list: blank line(s) between items of the same style
|
|
@@ -1166,8 +1140,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1166
1140
|
// parse.
|
|
1167
1141
|
const {
|
|
1168
1142
|
defs,
|
|
1169
|
-
cleaned
|
|
1170
|
-
lineMap
|
|
1143
|
+
cleaned
|
|
1171
1144
|
} = extractLinkDefinitions(input);
|
|
1172
1145
|
const inherited = baseOpts.linkDefs;
|
|
1173
1146
|
let linkDefs;
|
|
@@ -1184,22 +1157,8 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1184
1157
|
} : baseOpts;
|
|
1185
1158
|
const lines = cleaned.split('\n');
|
|
1186
1159
|
const blocks = [];
|
|
1187
|
-
// The line each block started on, parallel to `blocks`. Only collected when
|
|
1188
|
-
// ranges were asked for; a block's end is resolved after the loop, since the
|
|
1189
|
-
// branch that produced it has already moved `index` past whatever it read.
|
|
1190
|
-
const blockStartLines = opts.sourceRanges ? [] : null;
|
|
1191
|
-
// Set only by a block that consumes blank lines as content, where the
|
|
1192
|
-
// positional end derivation would trim them away.
|
|
1193
|
-
const blockEndLines = opts.sourceRanges ? [] : null;
|
|
1194
|
-
let blockStartLine = 0;
|
|
1195
|
-
const pushBlock = (node, endLine) => {
|
|
1196
|
-
blocks.push(node);
|
|
1197
|
-
blockStartLines?.push(blockStartLine);
|
|
1198
|
-
blockEndLines?.push(endLine);
|
|
1199
|
-
};
|
|
1200
1160
|
let index = 0;
|
|
1201
1161
|
while (index < lines.length) {
|
|
1202
|
-
blockStartLine = index;
|
|
1203
1162
|
const line = lines[index];
|
|
1204
1163
|
if (line.trim() === '') {
|
|
1205
1164
|
index++;
|
|
@@ -1218,21 +1177,18 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1218
1177
|
index++;
|
|
1219
1178
|
}
|
|
1220
1179
|
index++; // skip closing fence
|
|
1221
|
-
|
|
1222
|
-
// can end on them, so it states its own end rather than letting the
|
|
1223
|
-
// positional derivation trim them off.
|
|
1224
|
-
pushBlock({
|
|
1180
|
+
blocks.push({
|
|
1225
1181
|
type: 'codeblock',
|
|
1226
1182
|
language,
|
|
1227
1183
|
content: codeLines.join('\n')
|
|
1228
|
-
}
|
|
1184
|
+
});
|
|
1229
1185
|
continue;
|
|
1230
1186
|
}
|
|
1231
1187
|
|
|
1232
1188
|
// --- Heading ---
|
|
1233
1189
|
const headingMatch = line.match(/^(#{1,6}) +(.*)/);
|
|
1234
1190
|
if (headingMatch) {
|
|
1235
|
-
|
|
1191
|
+
blocks.push({
|
|
1236
1192
|
type: 'heading',
|
|
1237
1193
|
level: headingMatch[1].length,
|
|
1238
1194
|
children: parseInlineEntry(headingMatch[2], opts)
|
|
@@ -1243,7 +1199,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1243
1199
|
|
|
1244
1200
|
// --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
|
|
1245
1201
|
if (isHorizontalRule(line)) {
|
|
1246
|
-
|
|
1202
|
+
blocks.push({
|
|
1247
1203
|
type: 'hr'
|
|
1248
1204
|
});
|
|
1249
1205
|
index++;
|
|
@@ -1253,7 +1209,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1253
1209
|
// --- Standalone image ---
|
|
1254
1210
|
const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
|
|
1255
1211
|
if (imageMatch && line.trim() === imageMatch[0]) {
|
|
1256
|
-
|
|
1212
|
+
blocks.push({
|
|
1257
1213
|
type: 'image',
|
|
1258
1214
|
alt: imageMatch[1],
|
|
1259
1215
|
src: imageMatch[2]
|
|
@@ -1265,7 +1221,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1265
1221
|
// --- Table (with or without leading pipe) ---
|
|
1266
1222
|
if (index + 1 < lines.length && line.includes('|') && isTableSeparator(lines[index + 1])) {
|
|
1267
1223
|
const tableResult = parseTable(lines, index, opts);
|
|
1268
|
-
|
|
1224
|
+
blocks.push(tableResult.node);
|
|
1269
1225
|
index = tableResult.nextIndex;
|
|
1270
1226
|
continue;
|
|
1271
1227
|
}
|
|
@@ -1277,9 +1233,9 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1277
1233
|
quoteLines.push(lines[index].replace(/^> ?/, ''));
|
|
1278
1234
|
index++;
|
|
1279
1235
|
}
|
|
1280
|
-
|
|
1236
|
+
blocks.push({
|
|
1281
1237
|
type: 'blockquote',
|
|
1282
|
-
children: parseMarkdownImpl(quoteLines.join('\n'),
|
|
1238
|
+
children: parseMarkdownImpl(quoteLines.join('\n'), opts)
|
|
1283
1239
|
});
|
|
1284
1240
|
continue;
|
|
1285
1241
|
}
|
|
@@ -1287,7 +1243,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1287
1243
|
// --- Unordered list ---
|
|
1288
1244
|
if (/^ {0,9}[-*+] /.test(line)) {
|
|
1289
1245
|
const listResult = parseList(lines, index, false, opts);
|
|
1290
|
-
|
|
1246
|
+
blocks.push(listResult.node);
|
|
1291
1247
|
index = listResult.nextIndex;
|
|
1292
1248
|
continue;
|
|
1293
1249
|
}
|
|
@@ -1295,7 +1251,7 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1295
1251
|
// --- Ordered list ---
|
|
1296
1252
|
if (/^ {0,9}\d+[.)] /.test(line)) {
|
|
1297
1253
|
const listResult = parseList(lines, index, true, opts);
|
|
1298
|
-
|
|
1254
|
+
blocks.push(listResult.node);
|
|
1299
1255
|
index = listResult.nextIndex;
|
|
1300
1256
|
continue;
|
|
1301
1257
|
}
|
|
@@ -1307,60 +1263,14 @@ function parseMarkdownImpl(input, baseOpts) {
|
|
|
1307
1263
|
paraLines.push(lines[index]);
|
|
1308
1264
|
index++;
|
|
1309
1265
|
}
|
|
1310
|
-
|
|
1266
|
+
blocks.push({
|
|
1311
1267
|
type: 'paragraph',
|
|
1312
1268
|
children: parseInlineEntry(paraLines.join('\n'), opts)
|
|
1313
1269
|
});
|
|
1314
1270
|
}
|
|
1315
|
-
if (blockStartLines != null) {
|
|
1316
|
-
stampSourceRanges(blocks, blockStartLines, blockEndLines ?? [], lines, lineMap, input, opts);
|
|
1317
|
-
}
|
|
1318
1271
|
return blocks;
|
|
1319
1272
|
}
|
|
1320
1273
|
|
|
1321
|
-
/**
|
|
1322
|
-
* Give each block the offsets it occupies in the original input.
|
|
1323
|
-
*
|
|
1324
|
-
* Blocks are contiguous and in source order, so a block runs from its own
|
|
1325
|
-
* first line to the line before the next block starts, minus the blank lines
|
|
1326
|
-
* between them. Offsets are computed against the *input*, not the text the
|
|
1327
|
-
* block loop saw: link reference definitions are stripped before parsing, and
|
|
1328
|
-
* `lineMap` says which input line each surviving line came from.
|
|
1329
|
-
*/
|
|
1330
|
-
function stampSourceRanges(blocks, blockStartLines, blockEndLines, lines, lineMap, input, opts) {
|
|
1331
|
-
const base = opts.baseOffset ?? 0;
|
|
1332
|
-
// Offset of the first character of every line of the input.
|
|
1333
|
-
const inputLineStarts = [0];
|
|
1334
|
-
for (let i = 0; i < input.length; i++) {
|
|
1335
|
-
if (input[i] === '\n') {
|
|
1336
|
-
inputLineStarts.push(i + 1);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
// Stripping removes whole lines and never edits one, so a parsed line's
|
|
1340
|
-
// length is its input line's length.
|
|
1341
|
-
const lineStart = line => base + inputLineStarts[lineMap != null ? lineMap[line] : line];
|
|
1342
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1343
|
-
const startLine = blockStartLines[i];
|
|
1344
|
-
let endLine = blockEndLines[i];
|
|
1345
|
-
if (endLine == null) {
|
|
1346
|
-
const nextStart = i + 1 < blocks.length ? blockStartLines[i + 1] : lines.length;
|
|
1347
|
-
endLine = nextStart - 1;
|
|
1348
|
-
while (endLine > startLine && lines[endLine].trim() === '') {
|
|
1349
|
-
endLine--;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
// Exactly the block's own lines, verbatim — a CRLF document's trailing
|
|
1353
|
-
// `\r` included, since the parser reads it as part of the line too and a
|
|
1354
|
-
// range that dropped it would slice to something that re-parses
|
|
1355
|
-
// differently.
|
|
1356
|
-
const end = lineStart(endLine) + lines[endLine].length;
|
|
1357
|
-
blocks[i] = {
|
|
1358
|
-
...blocks[i],
|
|
1359
|
-
range: [lineStart(startLine), end]
|
|
1360
|
-
};
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1274
|
// ---------------------------------------------------------------------------
|
|
1365
1275
|
// Incremental parsing
|
|
1366
1276
|
// ---------------------------------------------------------------------------
|
|
@@ -1622,18 +1532,6 @@ function trimUnsettledStructural(text) {
|
|
|
1622
1532
|
return lines.join('\n');
|
|
1623
1533
|
}
|
|
1624
1534
|
|
|
1625
|
-
/**
|
|
1626
|
-
* The same options, for parsing a slice that starts at `offset` of the
|
|
1627
|
-
* document — so the slice's blocks report ranges into the whole document
|
|
1628
|
-
* rather than into the slice.
|
|
1629
|
-
*/
|
|
1630
|
-
function atOffset(opts, offset) {
|
|
1631
|
-
return opts.sourceRanges ? {
|
|
1632
|
-
...opts,
|
|
1633
|
-
baseOffset: offset
|
|
1634
|
-
} : opts;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1535
|
/**
|
|
1638
1536
|
* Concatenate freshly-parsed delta blocks with previously-settled blocks,
|
|
1639
1537
|
* merging adjacent same-style lists into a single loose list. The boundary
|
|
@@ -1654,12 +1552,7 @@ function mergeSettledBlocks(prev, delta) {
|
|
|
1654
1552
|
start: prevLast.start,
|
|
1655
1553
|
delimiter: prevLast.delimiter,
|
|
1656
1554
|
loose: true,
|
|
1657
|
-
items: [...prevLast.items, ...deltaFirst.items]
|
|
1658
|
-
// One list now, so one range: from where the first half started to
|
|
1659
|
-
// where the second half ended.
|
|
1660
|
-
...(prevLast.range != null && deltaFirst.range != null ? {
|
|
1661
|
-
range: [prevLast.range[0], deltaFirst.range[1]]
|
|
1662
|
-
} : null)
|
|
1555
|
+
items: [...prevLast.items, ...deltaFirst.items]
|
|
1663
1556
|
};
|
|
1664
1557
|
return [...prev.slice(0, -1), merged, ...delta.slice(1)];
|
|
1665
1558
|
}
|
|
@@ -1667,16 +1560,15 @@ function mergeSettledBlocks(prev, delta) {
|
|
|
1667
1560
|
}
|
|
1668
1561
|
export function parseMarkdownIncremental(input, state, arg) {
|
|
1669
1562
|
const opts = resolveOptions(arg);
|
|
1670
|
-
// Invalidate cache when the autolink
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1673
|
-
if (state.autolink !== opts.autolink
|
|
1563
|
+
// Invalidate cache when the autolink option flips — cached settled blocks
|
|
1564
|
+
// were parsed with the previous setting and would otherwise be reused
|
|
1565
|
+
// unchanged.
|
|
1566
|
+
if (state.autolink !== opts.autolink) {
|
|
1674
1567
|
state.prevInput = '';
|
|
1675
1568
|
state.settledText = '';
|
|
1676
1569
|
state.settledBlocks = [];
|
|
1677
1570
|
state.settledUpTo = 0;
|
|
1678
1571
|
state.autolink = opts.autolink;
|
|
1679
|
-
state.sourceRanges = opts.sourceRanges;
|
|
1680
1572
|
}
|
|
1681
1573
|
if (input === '') {
|
|
1682
1574
|
state.prevInput = '';
|
|
@@ -1723,20 +1615,13 @@ export function parseMarkdownIncremental(input, state, arg) {
|
|
|
1723
1615
|
} else if (state.settledText.length > 0 && settledText.startsWith(state.settledText)) {
|
|
1724
1616
|
// Settled portion grew — parse only the new delta
|
|
1725
1617
|
const delta = settledText.slice(state.settledText.length);
|
|
1726
|
-
const deltaBlocks = parseMarkdownImpl(delta,
|
|
1618
|
+
const deltaBlocks = parseMarkdownImpl(delta, parseOpts);
|
|
1727
1619
|
settledBlocks = mergeSettledBlocks(state.settledBlocks, deltaBlocks);
|
|
1728
1620
|
} else {
|
|
1729
1621
|
// Content before the boundary changed — full re-parse of settled portion
|
|
1730
1622
|
settledBlocks = parseMarkdownImpl(settledText, parseOpts);
|
|
1731
1623
|
}
|
|
1732
|
-
|
|
1733
|
-
// The unsettled tail is trimmed before parsing, so its offset in the
|
|
1734
|
-
// document is where that trimmed text actually starts — not the boundary,
|
|
1735
|
-
// which is a line index. If it somehow can't be located, parse it without
|
|
1736
|
-
// ranges rather than report wrong ones. Only worth searching for when
|
|
1737
|
-
// ranges were asked for: this runs on every streamed chunk.
|
|
1738
|
-
const unsettledStart = unsettledText && opts.sourceRanges ? input.indexOf(unsettledText, settledText.length) : -1;
|
|
1739
|
-
const unsettledBlocks = unsettledText ? parseMarkdownImpl(unsettledText, unsettledStart >= 0 ? atOffset(parseOpts, unsettledStart) : nested(parseOpts)) : [];
|
|
1624
|
+
const unsettledBlocks = unsettledText ? parseMarkdownImpl(unsettledText, parseOpts) : [];
|
|
1740
1625
|
state.settledText = settledText;
|
|
1741
1626
|
state.settledBlocks = settledBlocks;
|
|
1742
1627
|
state.settledUpTo = boundary;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.4.7-canary.
|
|
3
|
+
"version": "0.4.7-canary.ecaf779",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
"react-dom": ">=19.0.0"
|
|
662
662
|
},
|
|
663
663
|
"devDependencies": {
|
|
664
|
-
"@astryxdesign/cli": "0.4.7-canary.
|
|
664
|
+
"@astryxdesign/cli": "0.4.7-canary.ecaf779",
|
|
665
665
|
"@babel/cli": "^8.0.4",
|
|
666
666
|
"@babel/core": "^7.29.7",
|
|
667
667
|
"@babel/preset-react": "^8.0.1",
|
package/src/Markdown/index.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
parseInline,
|
|
7
|
-
parseMarkdown,
|
|
8
|
-
parseMarkdownIncremental,
|
|
9
|
-
} from './parser';
|
|
10
|
-
import type {BlockNode, InlineNode} from './parser';
|
|
4
|
+
import {parseMarkdown, parseInline} from './parser';
|
|
5
|
+
import type {InlineNode} from './parser';
|
|
11
6
|
|
|
12
7
|
describe('parseInline', () => {
|
|
13
8
|
it('parses plain text', () => {
|
|
@@ -1214,129 +1209,3 @@ describe('link reference definitions', () => {
|
|
|
1214
1209
|
expect(blocks[0].type).toBe('paragraph');
|
|
1215
1210
|
});
|
|
1216
1211
|
});
|
|
1217
|
-
|
|
1218
|
-
describe('sourceRanges', () => {
|
|
1219
|
-
const slice = (source: string, block: {range?: readonly [number, number]}) =>
|
|
1220
|
-
block.range == null ? null : source.slice(block.range[0], block.range[1]);
|
|
1221
|
-
|
|
1222
|
-
it('is off by default', () => {
|
|
1223
|
-
const [block] = parseMarkdown('# Title');
|
|
1224
|
-
expect(block.range).toBeUndefined();
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
|
-
it('gives every top-level block the source it came from', () => {
|
|
1228
|
-
const source = [
|
|
1229
|
-
'# Title',
|
|
1230
|
-
'',
|
|
1231
|
-
'A paragraph that',
|
|
1232
|
-
'wraps onto two lines.',
|
|
1233
|
-
'',
|
|
1234
|
-
'- one',
|
|
1235
|
-
'- two',
|
|
1236
|
-
'',
|
|
1237
|
-
'```js',
|
|
1238
|
-
'const x = 1;',
|
|
1239
|
-
'```',
|
|
1240
|
-
'',
|
|
1241
|
-
'| a | b |',
|
|
1242
|
-
'| --- | --- |',
|
|
1243
|
-
'| 1 | 2 |',
|
|
1244
|
-
'',
|
|
1245
|
-
'> quoted',
|
|
1246
|
-
'',
|
|
1247
|
-
'---',
|
|
1248
|
-
].join('\n');
|
|
1249
|
-
const blocks = parseMarkdown(source, {sourceRanges: true});
|
|
1250
|
-
expect(blocks.map(b => slice(source, b))).toEqual([
|
|
1251
|
-
'# Title',
|
|
1252
|
-
'A paragraph that\nwraps onto two lines.',
|
|
1253
|
-
'- one\n- two',
|
|
1254
|
-
'```js\nconst x = 1;\n```',
|
|
1255
|
-
'| a | b |\n| --- | --- |\n| 1 | 2 |',
|
|
1256
|
-
'> quoted',
|
|
1257
|
-
'---',
|
|
1258
|
-
]);
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
it('reports offsets into the input, not into the link-definition-stripped text', () => {
|
|
1262
|
-
// The definition lines are removed before the block loop runs, so a naive
|
|
1263
|
-
// offset would drift by their length for everything after them.
|
|
1264
|
-
const source = [
|
|
1265
|
-
'[ref]: https://example.com',
|
|
1266
|
-
'',
|
|
1267
|
-
'See [the docs][ref].',
|
|
1268
|
-
'',
|
|
1269
|
-
'Another paragraph.',
|
|
1270
|
-
].join('\n');
|
|
1271
|
-
const blocks = parseMarkdown(source, {sourceRanges: true});
|
|
1272
|
-
expect(blocks.map(b => slice(source, b))).toEqual([
|
|
1273
|
-
'See [the docs][ref].',
|
|
1274
|
-
'Another paragraph.',
|
|
1275
|
-
]);
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
it('reports absolute offsets when the document is parsed incrementally', () => {
|
|
1279
|
-
const source = ['# Title', '', 'One.', '', 'Two.', '', 'Three.'].join('\n');
|
|
1280
|
-
const state = createIncrementalState();
|
|
1281
|
-
let blocks: BlockNode[] = [];
|
|
1282
|
-
for (let end = 1; end <= source.length; end++) {
|
|
1283
|
-
blocks = parseMarkdownIncremental(source.slice(0, end), state, {
|
|
1284
|
-
sourceRanges: true,
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
expect(blocks.map(b => slice(source, b))).toEqual([
|
|
1288
|
-
'# Title',
|
|
1289
|
-
'One.',
|
|
1290
|
-
'Two.',
|
|
1291
|
-
'Three.',
|
|
1292
|
-
]);
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
it('keeps the blank lines an unterminated fence owns', () => {
|
|
1296
|
-
// Mid-stream the closing fence has not arrived, and the blank lines are
|
|
1297
|
-
// part of the code, not spacing between blocks.
|
|
1298
|
-
const source = '```\ncode\n\n';
|
|
1299
|
-
const [block] = parseMarkdown(source, {sourceRanges: true});
|
|
1300
|
-
// The whole thing: the fence consumed those lines as code.
|
|
1301
|
-
expect(slice(source, block)).toBe(source);
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
it('slices to something that re-parses to the same block', () => {
|
|
1305
|
-
// The property a consumer actually needs, and the one that says the
|
|
1306
|
-
// offsets are right: what the range points at is the block.
|
|
1307
|
-
const check = (source: string) => {
|
|
1308
|
-
for (const block of parseMarkdown(source, {sourceRanges: true})) {
|
|
1309
|
-
const {range: _range, ...node} = block;
|
|
1310
|
-
expect(parseMarkdown(slice(source, block)!)).toEqual([node]);
|
|
1311
|
-
}
|
|
1312
|
-
};
|
|
1313
|
-
check('# Title\n\nA paragraph.\n\n- one\n- two\n\n> quoted');
|
|
1314
|
-
// CRLF: the parser keeps the `\r` in its own content, so the range does
|
|
1315
|
-
// too rather than slicing to text that parses differently.
|
|
1316
|
-
check('# Title\r\n\r\nA paragraph.\r\n');
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
it('re-parses when the caller flips the option on an existing state', () => {
|
|
1320
|
-
const source = 'One.\n\nTwo.\n\nThree.';
|
|
1321
|
-
const state = createIncrementalState();
|
|
1322
|
-
const without = parseMarkdownIncremental(source, state);
|
|
1323
|
-
expect(without.every(b => b.range == null)).toBe(true);
|
|
1324
|
-
const with_ = parseMarkdownIncremental(source, state, {
|
|
1325
|
-
sourceRanges: true,
|
|
1326
|
-
});
|
|
1327
|
-
expect(with_.map(b => slice(source, b))).toEqual(['One.', 'Two.', 'Three.']);
|
|
1328
|
-
const back = parseMarkdownIncremental(source, state);
|
|
1329
|
-
expect(back.every(b => b.range == null)).toBe(true);
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
|
-
it('spans both halves of a list the incremental parser merged', () => {
|
|
1333
|
-
const source = '1. one\n\n1. two';
|
|
1334
|
-
const state = createIncrementalState();
|
|
1335
|
-
parseMarkdownIncremental('1. one\n\n', state, {sourceRanges: true});
|
|
1336
|
-
const blocks = parseMarkdownIncremental(source, state, {
|
|
1337
|
-
sourceRanges: true,
|
|
1338
|
-
});
|
|
1339
|
-
expect(blocks).toHaveLength(1);
|
|
1340
|
-
expect(slice(source, blocks[0])).toBe(source);
|
|
1341
|
-
});
|
|
1342
|
-
});
|
package/src/Markdown/parser.ts
CHANGED
|
@@ -24,15 +24,7 @@ export type InlineNode =
|
|
|
24
24
|
| {type: 'citation'; sourceId: string}
|
|
25
25
|
| {type: 'break'};
|
|
26
26
|
|
|
27
|
-
export type BlockNode =
|
|
28
|
-
/**
|
|
29
|
-
* Where this block came from in the source, when parsed with the
|
|
30
|
-
* `sourceRanges` option. Top-level blocks only.
|
|
31
|
-
*/
|
|
32
|
-
range?: SourceRange;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type BlockNodeKind =
|
|
27
|
+
export type BlockNode =
|
|
36
28
|
| {type: 'heading'; level: 1 | 2 | 3 | 4 | 5 | 6; children: InlineNode[]}
|
|
37
29
|
| {type: 'paragraph'; children: InlineNode[]}
|
|
38
30
|
| {type: 'codeblock'; language: string; content: string}
|
|
@@ -55,12 +47,6 @@ type BlockNodeKind =
|
|
|
55
47
|
| {type: 'hr'}
|
|
56
48
|
| {type: 'image'; src: string; alt: string};
|
|
57
49
|
|
|
58
|
-
/**
|
|
59
|
-
* Character offsets `[start, end)` of a block in the source string handed to
|
|
60
|
-
* `parseMarkdown`. `end` excludes the block's trailing blank lines.
|
|
61
|
-
*/
|
|
62
|
-
export type SourceRange = readonly [start: number, end: number];
|
|
63
|
-
|
|
64
50
|
export type ListItemNode = {checked?: boolean; children: BlockNode[]};
|
|
65
51
|
export type TableCellNode = {children: InlineNode[]};
|
|
66
52
|
export type TableAlignment = 'left' | 'center' | 'right' | null;
|
|
@@ -92,28 +78,11 @@ export type ParseOptions = {
|
|
|
92
78
|
* suffix is not rejected (Astryx accepts any plausible TLD shape).
|
|
93
79
|
*/
|
|
94
80
|
autolink?: 'gfm';
|
|
95
|
-
/**
|
|
96
|
-
* When true, every top-level block carries a `range` — the offsets it
|
|
97
|
-
* occupies in the string passed in. Lets a consumer that still holds the
|
|
98
|
-
* source slice the original markdown for a block instead of reconstructing
|
|
99
|
-
* it from the parsed node (or from the rendered DOM). Off by default: the
|
|
100
|
-
* field is absent unless asked for, so nothing that compares nodes changes.
|
|
101
|
-
*
|
|
102
|
-
* Blocks nested inside a list item or a blockquote do not carry one.
|
|
103
|
-
*/
|
|
104
|
-
sourceRanges?: boolean;
|
|
105
81
|
};
|
|
106
82
|
|
|
107
83
|
type ResolvedOptions = {
|
|
108
84
|
readonly sourceIds: ReadonlySet<string> | undefined;
|
|
109
85
|
readonly autolink: 'gfm' | undefined;
|
|
110
|
-
readonly sourceRanges?: boolean;
|
|
111
|
-
/**
|
|
112
|
-
* Offset of this parse's input within the document the ranges are reported
|
|
113
|
-
* against. Internal only — the incremental parser parses slices and needs
|
|
114
|
-
* their blocks' ranges to come out absolute.
|
|
115
|
-
*/
|
|
116
|
-
readonly baseOffset?: number;
|
|
117
86
|
/**
|
|
118
87
|
* Link reference definitions (`[label]: url`) collected from the whole
|
|
119
88
|
* document, keyed by normalized label. Internal only — populated by the
|
|
@@ -140,11 +109,7 @@ function resolveOptions(
|
|
|
140
109
|
return {sourceIds: arg as ReadonlySet<string>, autolink: undefined};
|
|
141
110
|
}
|
|
142
111
|
const opts = arg as ParseOptions;
|
|
143
|
-
return {
|
|
144
|
-
sourceIds: opts.sourceIds,
|
|
145
|
-
autolink: opts.autolink,
|
|
146
|
-
sourceRanges: opts.sourceRanges,
|
|
147
|
-
};
|
|
112
|
+
return {sourceIds: opts.sourceIds, autolink: opts.autolink};
|
|
148
113
|
}
|
|
149
114
|
|
|
150
115
|
// ---------------------------------------------------------------------------
|
|
@@ -209,11 +174,6 @@ function matchLinkDefinition(
|
|
|
209
174
|
function extractLinkDefinitions(input: string): {
|
|
210
175
|
defs: ReadonlyMap<string, string>;
|
|
211
176
|
cleaned: string;
|
|
212
|
-
/**
|
|
213
|
-
* For each line of `cleaned`, the line of `input` it came from. Undefined
|
|
214
|
-
* when nothing was stripped and the two are the same text.
|
|
215
|
-
*/
|
|
216
|
-
lineMap?: number[];
|
|
217
177
|
} {
|
|
218
178
|
const lines = input.split('\n');
|
|
219
179
|
const defs = new Map<string, string>();
|
|
@@ -275,14 +235,8 @@ function extractLinkDefinitions(input: string): {
|
|
|
275
235
|
if (defs.size === 0) {
|
|
276
236
|
return {defs, cleaned: input};
|
|
277
237
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
if (keep[index]) {
|
|
281
|
-
lineMap.push(index);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const cleaned = lineMap.map(index => lines[index]).join('\n');
|
|
285
|
-
return {defs, cleaned, lineMap};
|
|
238
|
+
const cleaned = lines.filter((_, index) => keep[index]).join('\n');
|
|
239
|
+
return {defs, cleaned};
|
|
286
240
|
}
|
|
287
241
|
|
|
288
242
|
/** Order-independent signature of a link-definition set, for cache checks. */
|
|
@@ -1088,16 +1042,6 @@ function isTableSeparator(line: string): boolean {
|
|
|
1088
1042
|
return nonEmpty.length > 0 && nonEmpty.every(cell => /^:?-+:?$/.test(cell));
|
|
1089
1043
|
}
|
|
1090
1044
|
|
|
1091
|
-
/**
|
|
1092
|
-
* The same options for content parsed out of an enclosing block. Ranges are a
|
|
1093
|
-
* top-level contract: a list item's or a blockquote's children are parsed from
|
|
1094
|
-
* text the caller reassembled (markers and `>` prefixes stripped), so an
|
|
1095
|
-
* offset into it would not address the document.
|
|
1096
|
-
*/
|
|
1097
|
-
function nested(opts: ResolvedOptions): ResolvedOptions {
|
|
1098
|
-
return opts.sourceRanges ? {...opts, sourceRanges: false} : opts;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
1045
|
/**
|
|
1102
1046
|
* Returns true when a line could start a new block — used to stop paragraph
|
|
1103
1047
|
* continuation. Every regex here uses bounded or single-class quantifiers
|
|
@@ -1269,7 +1213,7 @@ function parseList(
|
|
|
1269
1213
|
itemText += '\n' + deindented.join('\n');
|
|
1270
1214
|
}
|
|
1271
1215
|
|
|
1272
|
-
items.push({checked, children: parseMarkdownImpl(itemText,
|
|
1216
|
+
items.push({checked, children: parseMarkdownImpl(itemText, opts)});
|
|
1273
1217
|
|
|
1274
1218
|
// CommonMark loose list: blank line(s) between items of the same style
|
|
1275
1219
|
// and indent still form one list. Skip the blanks and continue if the
|
|
@@ -1330,7 +1274,7 @@ function parseMarkdownImpl(
|
|
|
1330
1274
|
// definitions win on conflict, matching CommonMark's first-definition-wins
|
|
1331
1275
|
// in document order; locally-nested definitions still resolve within this
|
|
1332
1276
|
// parse.
|
|
1333
|
-
const {defs, cleaned
|
|
1277
|
+
const {defs, cleaned} = extractLinkDefinitions(input);
|
|
1334
1278
|
const inherited = baseOpts.linkDefs;
|
|
1335
1279
|
let linkDefs: ReadonlyMap<string, string> | undefined;
|
|
1336
1280
|
if (defs.size === 0) {
|
|
@@ -1344,25 +1288,9 @@ function parseMarkdownImpl(
|
|
|
1344
1288
|
linkDefs != null ? {...baseOpts, linkDefs} : baseOpts;
|
|
1345
1289
|
const lines = cleaned.split('\n');
|
|
1346
1290
|
const blocks: BlockNode[] = [];
|
|
1347
|
-
// The line each block started on, parallel to `blocks`. Only collected when
|
|
1348
|
-
// ranges were asked for; a block's end is resolved after the loop, since the
|
|
1349
|
-
// branch that produced it has already moved `index` past whatever it read.
|
|
1350
|
-
const blockStartLines: number[] | null = opts.sourceRanges ? [] : null;
|
|
1351
|
-
// Set only by a block that consumes blank lines as content, where the
|
|
1352
|
-
// positional end derivation would trim them away.
|
|
1353
|
-
const blockEndLines: (number | undefined)[] | null = opts.sourceRanges
|
|
1354
|
-
? []
|
|
1355
|
-
: null;
|
|
1356
|
-
let blockStartLine = 0;
|
|
1357
|
-
const pushBlock = (node: BlockNode, endLine?: number) => {
|
|
1358
|
-
blocks.push(node);
|
|
1359
|
-
blockStartLines?.push(blockStartLine);
|
|
1360
|
-
blockEndLines?.push(endLine);
|
|
1361
|
-
};
|
|
1362
1291
|
let index = 0;
|
|
1363
1292
|
|
|
1364
1293
|
while (index < lines.length) {
|
|
1365
|
-
blockStartLine = index;
|
|
1366
1294
|
const line = lines[index];
|
|
1367
1295
|
if (line.trim() === '') {
|
|
1368
1296
|
index++;
|
|
@@ -1381,20 +1309,14 @@ function parseMarkdownImpl(
|
|
|
1381
1309
|
index++;
|
|
1382
1310
|
}
|
|
1383
1311
|
index++; // skip closing fence
|
|
1384
|
-
|
|
1385
|
-
// can end on them, so it states its own end rather than letting the
|
|
1386
|
-
// positional derivation trim them off.
|
|
1387
|
-
pushBlock(
|
|
1388
|
-
{type: 'codeblock', language, content: codeLines.join('\n')},
|
|
1389
|
-
Math.min(index, lines.length) - 1,
|
|
1390
|
-
);
|
|
1312
|
+
blocks.push({type: 'codeblock', language, content: codeLines.join('\n')});
|
|
1391
1313
|
continue;
|
|
1392
1314
|
}
|
|
1393
1315
|
|
|
1394
1316
|
// --- Heading ---
|
|
1395
1317
|
const headingMatch = line.match(/^(#{1,6}) +(.*)/);
|
|
1396
1318
|
if (headingMatch) {
|
|
1397
|
-
|
|
1319
|
+
blocks.push({
|
|
1398
1320
|
type: 'heading',
|
|
1399
1321
|
level: headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6,
|
|
1400
1322
|
children: parseInlineEntry(headingMatch[2], opts),
|
|
@@ -1405,7 +1327,7 @@ function parseMarkdownImpl(
|
|
|
1405
1327
|
|
|
1406
1328
|
// --- HR (must precede list check to handle `- - -`, `* * *`, `_ _ _`) ---
|
|
1407
1329
|
if (isHorizontalRule(line)) {
|
|
1408
|
-
|
|
1330
|
+
blocks.push({type: 'hr'});
|
|
1409
1331
|
index++;
|
|
1410
1332
|
continue;
|
|
1411
1333
|
}
|
|
@@ -1413,7 +1335,7 @@ function parseMarkdownImpl(
|
|
|
1413
1335
|
// --- Standalone image ---
|
|
1414
1336
|
const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
|
|
1415
1337
|
if (imageMatch && line.trim() === imageMatch[0]) {
|
|
1416
|
-
|
|
1338
|
+
blocks.push({type: 'image', alt: imageMatch[1], src: imageMatch[2]});
|
|
1417
1339
|
index++;
|
|
1418
1340
|
continue;
|
|
1419
1341
|
}
|
|
@@ -1425,7 +1347,7 @@ function parseMarkdownImpl(
|
|
|
1425
1347
|
isTableSeparator(lines[index + 1])
|
|
1426
1348
|
) {
|
|
1427
1349
|
const tableResult = parseTable(lines, index, opts);
|
|
1428
|
-
|
|
1350
|
+
blocks.push(tableResult.node);
|
|
1429
1351
|
index = tableResult.nextIndex;
|
|
1430
1352
|
continue;
|
|
1431
1353
|
}
|
|
@@ -1440,9 +1362,9 @@ function parseMarkdownImpl(
|
|
|
1440
1362
|
quoteLines.push(lines[index].replace(/^> ?/, ''));
|
|
1441
1363
|
index++;
|
|
1442
1364
|
}
|
|
1443
|
-
|
|
1365
|
+
blocks.push({
|
|
1444
1366
|
type: 'blockquote',
|
|
1445
|
-
children: parseMarkdownImpl(quoteLines.join('\n'),
|
|
1367
|
+
children: parseMarkdownImpl(quoteLines.join('\n'), opts),
|
|
1446
1368
|
});
|
|
1447
1369
|
continue;
|
|
1448
1370
|
}
|
|
@@ -1450,7 +1372,7 @@ function parseMarkdownImpl(
|
|
|
1450
1372
|
// --- Unordered list ---
|
|
1451
1373
|
if (/^ {0,9}[-*+] /.test(line)) {
|
|
1452
1374
|
const listResult = parseList(lines, index, false, opts);
|
|
1453
|
-
|
|
1375
|
+
blocks.push(listResult.node);
|
|
1454
1376
|
index = listResult.nextIndex;
|
|
1455
1377
|
continue;
|
|
1456
1378
|
}
|
|
@@ -1458,7 +1380,7 @@ function parseMarkdownImpl(
|
|
|
1458
1380
|
// --- Ordered list ---
|
|
1459
1381
|
if (/^ {0,9}\d+[.)] /.test(line)) {
|
|
1460
1382
|
const listResult = parseList(lines, index, true, opts);
|
|
1461
|
-
|
|
1383
|
+
blocks.push(listResult.node);
|
|
1462
1384
|
index = listResult.nextIndex;
|
|
1463
1385
|
continue;
|
|
1464
1386
|
}
|
|
@@ -1474,76 +1396,14 @@ function parseMarkdownImpl(
|
|
|
1474
1396
|
paraLines.push(lines[index]);
|
|
1475
1397
|
index++;
|
|
1476
1398
|
}
|
|
1477
|
-
|
|
1399
|
+
blocks.push({
|
|
1478
1400
|
type: 'paragraph',
|
|
1479
1401
|
children: parseInlineEntry(paraLines.join('\n'), opts),
|
|
1480
1402
|
});
|
|
1481
1403
|
}
|
|
1482
|
-
if (blockStartLines != null) {
|
|
1483
|
-
stampSourceRanges(
|
|
1484
|
-
blocks,
|
|
1485
|
-
blockStartLines,
|
|
1486
|
-
blockEndLines ?? [],
|
|
1487
|
-
lines,
|
|
1488
|
-
lineMap,
|
|
1489
|
-
input,
|
|
1490
|
-
opts,
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
1404
|
return blocks;
|
|
1494
1405
|
}
|
|
1495
1406
|
|
|
1496
|
-
/**
|
|
1497
|
-
* Give each block the offsets it occupies in the original input.
|
|
1498
|
-
*
|
|
1499
|
-
* Blocks are contiguous and in source order, so a block runs from its own
|
|
1500
|
-
* first line to the line before the next block starts, minus the blank lines
|
|
1501
|
-
* between them. Offsets are computed against the *input*, not the text the
|
|
1502
|
-
* block loop saw: link reference definitions are stripped before parsing, and
|
|
1503
|
-
* `lineMap` says which input line each surviving line came from.
|
|
1504
|
-
*/
|
|
1505
|
-
function stampSourceRanges(
|
|
1506
|
-
blocks: BlockNode[],
|
|
1507
|
-
blockStartLines: number[],
|
|
1508
|
-
blockEndLines: (number | undefined)[],
|
|
1509
|
-
lines: string[],
|
|
1510
|
-
lineMap: number[] | undefined,
|
|
1511
|
-
input: string,
|
|
1512
|
-
opts: ResolvedOptions,
|
|
1513
|
-
): void {
|
|
1514
|
-
const base = opts.baseOffset ?? 0;
|
|
1515
|
-
// Offset of the first character of every line of the input.
|
|
1516
|
-
const inputLineStarts = [0];
|
|
1517
|
-
for (let i = 0; i < input.length; i++) {
|
|
1518
|
-
if (input[i] === '\n') {
|
|
1519
|
-
inputLineStarts.push(i + 1);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// Stripping removes whole lines and never edits one, so a parsed line's
|
|
1523
|
-
// length is its input line's length.
|
|
1524
|
-
const lineStart = (line: number): number =>
|
|
1525
|
-
base + inputLineStarts[lineMap != null ? lineMap[line] : line];
|
|
1526
|
-
|
|
1527
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1528
|
-
const startLine = blockStartLines[i];
|
|
1529
|
-
let endLine = blockEndLines[i];
|
|
1530
|
-
if (endLine == null) {
|
|
1531
|
-
const nextStart =
|
|
1532
|
-
i + 1 < blocks.length ? blockStartLines[i + 1] : lines.length;
|
|
1533
|
-
endLine = nextStart - 1;
|
|
1534
|
-
while (endLine > startLine && lines[endLine].trim() === '') {
|
|
1535
|
-
endLine--;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
// Exactly the block's own lines, verbatim — a CRLF document's trailing
|
|
1539
|
-
// `\r` included, since the parser reads it as part of the line too and a
|
|
1540
|
-
// range that dropped it would slice to something that re-parses
|
|
1541
|
-
// differently.
|
|
1542
|
-
const end = lineStart(endLine) + lines[endLine].length;
|
|
1543
|
-
blocks[i] = {...blocks[i], range: [lineStart(startLine), end]};
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
1407
|
// ---------------------------------------------------------------------------
|
|
1548
1408
|
// Incremental parsing
|
|
1549
1409
|
// ---------------------------------------------------------------------------
|
|
@@ -1560,12 +1420,6 @@ export interface IncrementalState {
|
|
|
1560
1420
|
* with newly-arriving content.
|
|
1561
1421
|
*/
|
|
1562
1422
|
autolink?: 'gfm';
|
|
1563
|
-
/**
|
|
1564
|
-
* The `sourceRanges` option the cached `settledBlocks` were parsed with.
|
|
1565
|
-
* Flipping it invalidates them the same way `autolink` does: they either
|
|
1566
|
-
* lack the ranges the caller now asks for, or carry ones it did not.
|
|
1567
|
-
*/
|
|
1568
|
-
sourceRanges?: boolean;
|
|
1569
1423
|
/**
|
|
1570
1424
|
* Signature of the link reference definitions the cached `settledBlocks`
|
|
1571
1425
|
* were parsed with. Definitions are document-global and typically arrive
|
|
@@ -1835,15 +1689,6 @@ function trimUnsettledStructural(text: string): string {
|
|
|
1835
1689
|
return lines.join('\n');
|
|
1836
1690
|
}
|
|
1837
1691
|
|
|
1838
|
-
/**
|
|
1839
|
-
* The same options, for parsing a slice that starts at `offset` of the
|
|
1840
|
-
* document — so the slice's blocks report ranges into the whole document
|
|
1841
|
-
* rather than into the slice.
|
|
1842
|
-
*/
|
|
1843
|
-
function atOffset(opts: ResolvedOptions, offset: number): ResolvedOptions {
|
|
1844
|
-
return opts.sourceRanges ? {...opts, baseOffset: offset} : opts;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
1692
|
/**
|
|
1848
1693
|
* Concatenate freshly-parsed delta blocks with previously-settled blocks,
|
|
1849
1694
|
* merging adjacent same-style lists into a single loose list. The boundary
|
|
@@ -1873,11 +1718,6 @@ function mergeSettledBlocks(
|
|
|
1873
1718
|
delimiter: prevLast.delimiter,
|
|
1874
1719
|
loose: true,
|
|
1875
1720
|
items: [...prevLast.items, ...deltaFirst.items],
|
|
1876
|
-
// One list now, so one range: from where the first half started to
|
|
1877
|
-
// where the second half ended.
|
|
1878
|
-
...(prevLast.range != null && deltaFirst.range != null
|
|
1879
|
-
? {range: [prevLast.range[0], deltaFirst.range[1]] as SourceRange}
|
|
1880
|
-
: null),
|
|
1881
1721
|
};
|
|
1882
1722
|
return [...prev.slice(0, -1), merged, ...delta.slice(1)];
|
|
1883
1723
|
}
|
|
@@ -1900,19 +1740,15 @@ export function parseMarkdownIncremental(
|
|
|
1900
1740
|
arg?: ReadonlySet<string> | ParseOptions,
|
|
1901
1741
|
): BlockNode[] {
|
|
1902
1742
|
const opts = resolveOptions(arg);
|
|
1903
|
-
// Invalidate cache when the autolink
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
1906
|
-
if (
|
|
1907
|
-
state.autolink !== opts.autolink ||
|
|
1908
|
-
Boolean(state.sourceRanges) !== Boolean(opts.sourceRanges)
|
|
1909
|
-
) {
|
|
1743
|
+
// Invalidate cache when the autolink option flips — cached settled blocks
|
|
1744
|
+
// were parsed with the previous setting and would otherwise be reused
|
|
1745
|
+
// unchanged.
|
|
1746
|
+
if (state.autolink !== opts.autolink) {
|
|
1910
1747
|
state.prevInput = '';
|
|
1911
1748
|
state.settledText = '';
|
|
1912
1749
|
state.settledBlocks = [];
|
|
1913
1750
|
state.settledUpTo = 0;
|
|
1914
1751
|
state.autolink = opts.autolink;
|
|
1915
|
-
state.sourceRanges = opts.sourceRanges;
|
|
1916
1752
|
}
|
|
1917
1753
|
if (input === '') {
|
|
1918
1754
|
state.prevInput = '';
|
|
@@ -1963,32 +1799,15 @@ export function parseMarkdownIncremental(
|
|
|
1963
1799
|
) {
|
|
1964
1800
|
// Settled portion grew — parse only the new delta
|
|
1965
1801
|
const delta = settledText.slice(state.settledText.length);
|
|
1966
|
-
const deltaBlocks = parseMarkdownImpl(
|
|
1967
|
-
delta,
|
|
1968
|
-
atOffset(parseOpts, state.settledText.length),
|
|
1969
|
-
);
|
|
1802
|
+
const deltaBlocks = parseMarkdownImpl(delta, parseOpts);
|
|
1970
1803
|
settledBlocks = mergeSettledBlocks(state.settledBlocks, deltaBlocks);
|
|
1971
1804
|
} else {
|
|
1972
1805
|
// Content before the boundary changed — full re-parse of settled portion
|
|
1973
1806
|
settledBlocks = parseMarkdownImpl(settledText, parseOpts);
|
|
1974
1807
|
}
|
|
1975
1808
|
|
|
1976
|
-
// The unsettled tail is trimmed before parsing, so its offset in the
|
|
1977
|
-
// document is where that trimmed text actually starts — not the boundary,
|
|
1978
|
-
// which is a line index. If it somehow can't be located, parse it without
|
|
1979
|
-
// ranges rather than report wrong ones. Only worth searching for when
|
|
1980
|
-
// ranges were asked for: this runs on every streamed chunk.
|
|
1981
|
-
const unsettledStart =
|
|
1982
|
-
unsettledText && opts.sourceRanges
|
|
1983
|
-
? input.indexOf(unsettledText, settledText.length)
|
|
1984
|
-
: -1;
|
|
1985
1809
|
const unsettledBlocks = unsettledText
|
|
1986
|
-
? parseMarkdownImpl(
|
|
1987
|
-
unsettledText,
|
|
1988
|
-
unsettledStart >= 0
|
|
1989
|
-
? atOffset(parseOpts, unsettledStart)
|
|
1990
|
-
: nested(parseOpts),
|
|
1991
|
-
)
|
|
1810
|
+
? parseMarkdownImpl(unsettledText, parseOpts)
|
|
1992
1811
|
: [];
|
|
1993
1812
|
|
|
1994
1813
|
state.settledText = settledText;
|