tarsec 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/combinators.d.ts +3 -0
- package/dist/combinators.js +3 -0
- package/dist/parsers/markdown/blocks.d.ts +3 -1
- package/dist/parsers/markdown/blocks.js +110 -29
- package/dist/parsers/markdown/index.js +4 -11
- package/dist/parsers/markdown/references.js +3 -3
- package/dist/parsers/markdown/types.d.ts +2 -2
- package/dist/parsers.d.ts +2 -0
- package/dist/parsers.js +2 -0
- package/package.json +1 -1
package/dist/combinators.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { CaptureParser, GeneralParser, InferManyReturnType, MergedCaptures, Merg
|
|
|
7
7
|
* { captures: <array of captures> }
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
+
* If you're parsing characters only, prefer `takeWhile` for performance reasons.
|
|
11
|
+
*
|
|
10
12
|
* @param parser - parser to run
|
|
11
13
|
* @returns - parser that runs the given parser zero to many times,
|
|
12
14
|
* and returns the result as an array
|
|
@@ -14,6 +16,7 @@ import { CaptureParser, GeneralParser, InferManyReturnType, MergedCaptures, Merg
|
|
|
14
16
|
export declare function many<const T extends GeneralParser<any, any>>(parser: T): InferManyReturnType<T>;
|
|
15
17
|
/**
|
|
16
18
|
* Same as `many`, but fails if the parser doesn't match at least once.
|
|
19
|
+
* If you're parsing characters only, prefer `takeWhile1` for performance reasons.
|
|
17
20
|
*
|
|
18
21
|
* @param parser - parser to run
|
|
19
22
|
* @returns a parser that runs the given parser one to many times,
|
package/dist/combinators.js
CHANGED
|
@@ -11,6 +11,8 @@ import { escape } from "./utils.js";
|
|
|
11
11
|
* { captures: <array of captures> }
|
|
12
12
|
* ```
|
|
13
13
|
*
|
|
14
|
+
* If you're parsing characters only, prefer `takeWhile` for performance reasons.
|
|
15
|
+
*
|
|
14
16
|
* @param parser - parser to run
|
|
15
17
|
* @returns - parser that runs the given parser zero to many times,
|
|
16
18
|
* and returns the result as an array
|
|
@@ -50,6 +52,7 @@ export function many(parser) {
|
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Same as `many`, but fails if the parser doesn't match at least once.
|
|
55
|
+
* If you're parsing characters only, prefer `takeWhile1` for performance reasons.
|
|
53
56
|
*
|
|
54
57
|
* @param parser - parser to run
|
|
55
58
|
* @returns a parser that runs the given parser one to many times,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Parser } from "../../types.js";
|
|
2
|
-
import { Heading, CodeBlock, BlockQuote, Paragraph, HorizontalRule, List, Table, HTMLBlock } from "./types.js";
|
|
2
|
+
import { Heading, CodeBlock, BlockQuote, Paragraph, HorizontalRule, List, Table, HTMLBlock, Block } from "./types.js";
|
|
3
3
|
export { imageParser } from "./inline.js";
|
|
4
4
|
export declare const headingParser: Parser<Heading>;
|
|
5
5
|
export declare const codeBlockParser: Parser<CodeBlock>;
|
|
@@ -12,3 +12,5 @@ export declare const htmlBlockParser: Parser<HTMLBlock>;
|
|
|
12
12
|
export declare const tableParser: Parser<Table>;
|
|
13
13
|
export declare const horizontalRuleParser: Parser<HorizontalRule>;
|
|
14
14
|
export declare const paragraphParser: Parser<Paragraph>;
|
|
15
|
+
export declare const blockAlt: Parser<Block>;
|
|
16
|
+
export declare const blockEntry: Parser<Block>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { seqC, seqR, capture, optional, or, manyTillStr, many1Till, exactly, many, many1, many1WithJoin, map, not, lazy, } from "../../combinators.js";
|
|
2
|
-
import { str, spaces, char, eof, set, alphanum, oneOf, noneOf, } from "../../parsers.js";
|
|
2
|
+
import { str, spaces, char, eof, set, alphanum, oneOf, noneOf, newline, } from "../../parsers.js";
|
|
3
|
+
import { success, failure } from "../../types.js";
|
|
4
|
+
import { linkDefinitionParser, footnoteDefinitionParser, } from "./references.js";
|
|
3
5
|
import { digit, letter } from "../../parsers.js";
|
|
4
6
|
import { manyTill } from "../../combinators.js";
|
|
5
|
-
import { inlineMarkdownParser, softBreakParser } from "./inline.js";
|
|
7
|
+
import { inlineMarkdownParser, imageParser, softBreakParser } from "./inline.js";
|
|
6
8
|
export { imageParser } from "./inline.js";
|
|
7
9
|
const languageChar = or(alphanum, oneOf("_+#.-"));
|
|
8
10
|
const languageTag = many1WithJoin(languageChar);
|
|
@@ -76,39 +78,110 @@ export const setextHeadingParser = map(_setextRaw, (caps) => {
|
|
|
76
78
|
: [{ type: "inline-text", content: caps.content }],
|
|
77
79
|
};
|
|
78
80
|
});
|
|
79
|
-
const unorderedMarker = map(oneOf("-*+"), () => ({ ord: false, start: 1 }));
|
|
80
|
-
const orderedMarker = map(seqC(capture(many1WithJoin(digit), "digits"), char(".")), ({ digits }) => ({
|
|
81
|
+
const unorderedMarker = map(oneOf("-*+"), () => ({ ord: false, start: 1, width: 1 }));
|
|
82
|
+
const orderedMarker = map(seqC(capture(many1WithJoin(digit), "digits"), char(".")), ({ digits }) => ({
|
|
83
|
+
ord: true,
|
|
84
|
+
start: parseInt(digits, 10),
|
|
85
|
+
width: digits.length + 1,
|
|
86
|
+
}));
|
|
87
|
+
const anyMarker = or(unorderedMarker, orderedMarker);
|
|
81
88
|
const indentOf = (n) => n > 0 ? str(" ".repeat(n)) : str("");
|
|
82
89
|
/* GFM task-list checkbox: `[ ]` (unchecked), `[x]` or `[X]` (checked).
|
|
83
90
|
* Must be followed by a single space (consumed) to count as a checkbox. */
|
|
84
91
|
const taskCheckbox = map(seqC(char("["), capture(or(char(" "), char("x"), char("X")), "mark"), str("] ")), ({ mark }) => mark !== " ");
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
/* The first line of an item's content — everything after the marker+space
|
|
93
|
+
* (and optional checkbox), up to `\n`. */
|
|
94
|
+
const itemFirstLine = map(seqC(capture(manyTillStr("\n"), "line"), or(char("\n"), eof)), ({ line }) => line);
|
|
95
|
+
/* A continuation line: exactly k spaces of indent, then the rest of the line.
|
|
96
|
+
* The strip IS the parse — `indentOf(k)` consumes the leading indent. */
|
|
97
|
+
const continuationLine = (k) => map(seqC(indentOf(k), capture(manyTillStr("\n"), "line"), or(char("\n"), eof)), ({ line }) => line);
|
|
98
|
+
/* A blank line at line-start. `blocks.ts` already defines `blankLine` below,
|
|
99
|
+
* but that variant starts with `\n` (designed for inter-block separators).
|
|
100
|
+
* Here we're already past the previous `\n`, so we need a line-start variant.
|
|
101
|
+
* The matched value isn't used as data; itemBody normalises blanks to "". */
|
|
102
|
+
const blankContinuation = seqR(many(oneOf(" \t")), or(char("\n"), eof));
|
|
103
|
+
/* One line of item body (after the first). Heterogeneous return type:
|
|
104
|
+
* - continuationLine -> string (the dedented line text)
|
|
105
|
+
* - blankContinuation -> unknown (discardable seqR output)
|
|
106
|
+
* itemBody normalises blanks to "" inline so a wrapper `map` is unnecessary. */
|
|
107
|
+
const itemContentLine = (k) => or(continuationLine(k), blankContinuation);
|
|
108
|
+
/* Reparse an item's collected body as a sequence of blocks via `blockEntry`.
|
|
109
|
+
*
|
|
110
|
+
* No silent fallback. CommonMark guarantees any non-empty buffer produces at
|
|
111
|
+
* least one block (worst case, a paragraph). The only legitimate empty case
|
|
112
|
+
* is an item whose first line is empty and which has no continuations — we
|
|
113
|
+
* handle that explicitly so the invariant `non-empty buf => at least one
|
|
114
|
+
* block` stays a real invariant. */
|
|
115
|
+
const reparseBlocks = (buf) => {
|
|
116
|
+
if (buf === "")
|
|
117
|
+
return [];
|
|
118
|
+
const r = many1(lazy(() => blockEntry))(buf);
|
|
119
|
+
if (!r.success) {
|
|
120
|
+
throw new Error(`reparseBlocks: failed on non-empty buffer: ${JSON.stringify(buf)}`);
|
|
121
|
+
}
|
|
122
|
+
return r.result;
|
|
94
123
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
124
|
+
/* Full item content: first line + zero-or-more continuation lines, joined
|
|
125
|
+
* with `\n` and reparsed. */
|
|
126
|
+
const itemBody = (k) => map(seqC(capture(itemFirstLine, "first"), capture(many(itemContentLine(k)), "rest")), ({ first, rest }) => {
|
|
127
|
+
const lines = [
|
|
128
|
+
first,
|
|
129
|
+
...rest.map((r) => (typeof r === "string" ? r : "")),
|
|
130
|
+
];
|
|
131
|
+
return reparseBlocks(lines.join("\n"));
|
|
103
132
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
/* Item parser at marker column c. Handwritten because `k` is derived from the
|
|
134
|
+
* parsed marker's width — `seqC` runs its children in fixed order with no data
|
|
135
|
+
* flow between them, so we sequence by hand to thread `marker.width` into
|
|
136
|
+
* `itemBody(k)`. Same shape as `inlineCodeParser` (inline.ts:179), which
|
|
137
|
+
* threads the opener's tick count into its closer for the same reason. */
|
|
138
|
+
const itemParser = (c) => (input) => {
|
|
139
|
+
const head = seqC(indentOf(c), capture(anyMarker, "marker"), char(" "), capture(optional(taskCheckbox), "checked"))(input);
|
|
140
|
+
if (!head.success)
|
|
141
|
+
return head;
|
|
142
|
+
const { marker, checked } = head.result;
|
|
143
|
+
const k = c + marker.width + 1;
|
|
144
|
+
const body = itemBody(k)(head.rest);
|
|
145
|
+
if (!body.success)
|
|
146
|
+
return body;
|
|
147
|
+
const item = { content: body.result };
|
|
148
|
+
if (checked !== null)
|
|
149
|
+
item.checked = checked;
|
|
150
|
+
return success({ marker, item }, body.rest);
|
|
151
|
+
};
|
|
152
|
+
/* An item whose marker family matches the locked one. Fails (without
|
|
153
|
+
* consuming) if the family changed. */
|
|
154
|
+
const itemMatching = (c, ord) => (input) => {
|
|
155
|
+
const r = itemParser(c)(input);
|
|
156
|
+
if (!r.success)
|
|
157
|
+
return r;
|
|
158
|
+
if (r.result.marker.ord !== ord) {
|
|
159
|
+
return failure("list marker family changed", input);
|
|
160
|
+
}
|
|
161
|
+
return r;
|
|
162
|
+
};
|
|
163
|
+
/* A list at column c. Marker family locked by the first item; subsequent
|
|
164
|
+
* items use `itemMatching` to enforce the lock. */
|
|
165
|
+
const listOf = (c) => (input) => {
|
|
166
|
+
const first = itemParser(c)(input);
|
|
167
|
+
if (!first.success)
|
|
168
|
+
return first;
|
|
169
|
+
const ord = first.result.marker.ord;
|
|
170
|
+
const rest = many(itemMatching(c, ord))(first.rest);
|
|
171
|
+
if (!rest.success)
|
|
172
|
+
return rest;
|
|
173
|
+
const items = [
|
|
174
|
+
first.result.item,
|
|
175
|
+
...rest.result.map((r) => r.item),
|
|
176
|
+
];
|
|
177
|
+
return success({
|
|
178
|
+
type: "list",
|
|
179
|
+
ordered: ord,
|
|
180
|
+
start: first.result.marker.start,
|
|
181
|
+
items,
|
|
182
|
+
}, rest.rest);
|
|
183
|
+
};
|
|
184
|
+
const listParserAt = (indent) => listOf(indent);
|
|
112
185
|
export const listParser = listParserAt(0);
|
|
113
186
|
/* Tables.
|
|
114
187
|
*
|
|
@@ -187,3 +260,11 @@ seqR(char("<"), or(letter, oneOf("/!?"))));
|
|
|
187
260
|
const paragraphSoftBreak = map(seqR(softBreakParser, not(blockInterrupt)), () => ({ type: "inline-soft-break" }));
|
|
188
261
|
const paragraphInline = map(seqC(not(blankLine), capture(or(paragraphSoftBreak, inlineMarkdownParser), "node")), ({ node }) => node);
|
|
189
262
|
export const paragraphParser = map(many1(paragraphInline), (content) => ({ type: "paragraph", content: content }));
|
|
263
|
+
/* Top-level block dispatcher. Lives here (rather than in index.ts) so
|
|
264
|
+
* `listParser`'s `reparseBlocks` can recurse through it via `lazy`. */
|
|
265
|
+
export const blockAlt = or(setextHeadingParser, horizontalRuleParser, headingParser, codeBlockParser, indentedCodeBlockParser, tableParser, blockQuoteParser, listParser, htmlBlockParser, linkDefinitionParser, footnoteDefinitionParser, paragraphParser, imageParser);
|
|
266
|
+
/* A block followed by zero-or-more trailing newlines. Blocks differ in
|
|
267
|
+
* whether they consume their own terminating "\n" (e.g. headingParser does,
|
|
268
|
+
* codeBlock doesn't), so `sepBy(many1(newline), block)` won't work — it
|
|
269
|
+
* would fail to separate two blocks when the first already ate its newline. */
|
|
270
|
+
export const blockEntry = map(seqC(capture(blockAlt, "b"), many(newline)), ({ b }) => b);
|
|
@@ -3,18 +3,11 @@ export * from "./inline.js";
|
|
|
3
3
|
export * from "./blocks.js";
|
|
4
4
|
export * from "./references.js";
|
|
5
5
|
export * from "./frontmatter.js";
|
|
6
|
-
import { seq,
|
|
7
|
-
import { spaces
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { seq, optional, many, map } from "../../combinators.js";
|
|
7
|
+
import { spaces } from "../../parsers.js";
|
|
8
|
+
import { blockEntry } from "./blocks.js";
|
|
9
|
+
import { resolveReferences } from "./references.js";
|
|
10
10
|
import { frontmatterParser } from "./frontmatter.js";
|
|
11
|
-
const blockAlt = or(setextHeadingParser, horizontalRuleParser, headingParser, codeBlockParser, indentedCodeBlockParser, tableParser, blockQuoteParser, listParser, htmlBlockParser, linkDefinitionParser, footnoteDefinitionParser, paragraphParser, imageParser);
|
|
12
|
-
// A block followed by zero-or-more trailing newlines. Blocks differ in whether
|
|
13
|
-
// they consume their own terminating "\n" (e.g. headingParser does, codeBlock
|
|
14
|
-
// doesn't), so we can't use sepBy(many1(newline), block) — it would fail to
|
|
15
|
-
// separate two blocks when the first already ate its newline (e.g. a heading
|
|
16
|
-
// directly followed by a list with no intervening blank line).
|
|
17
|
-
const blockEntry = map(seqC(capture(blockAlt, "b"), many(newline)), ({ b }) => b);
|
|
18
11
|
const _markdownParser = seq([
|
|
19
12
|
optional(frontmatterParser),
|
|
20
13
|
optional(spaces),
|
|
@@ -75,14 +75,14 @@ export function resolveReferences(ast) {
|
|
|
75
75
|
}
|
|
76
76
|
return { type: "inline-text", content: `[^${obj.id}]` };
|
|
77
77
|
}
|
|
78
|
-
// recurse into known child-bearing fields
|
|
78
|
+
// recurse into known child-bearing fields. Nested lists now live inside
|
|
79
|
+
// a list item's `content` (alongside other block children), so no
|
|
80
|
+
// dedicated `sublist` traversal is needed.
|
|
79
81
|
const out = Object.assign({}, obj);
|
|
80
82
|
for (const key of ["content", "items", "rows"]) {
|
|
81
83
|
if (Array.isArray(obj[key]))
|
|
82
84
|
out[key] = obj[key].map(walk);
|
|
83
85
|
}
|
|
84
|
-
if (obj.sublist)
|
|
85
|
-
out.sublist = walk(obj.sublist);
|
|
86
86
|
return out;
|
|
87
87
|
}
|
|
88
88
|
return ast
|
|
@@ -76,11 +76,11 @@ export type InlineRefImage = {
|
|
|
76
76
|
id: string;
|
|
77
77
|
};
|
|
78
78
|
export type ListItem = {
|
|
79
|
-
content:
|
|
80
|
-
sublist?: List;
|
|
79
|
+
content: Block[];
|
|
81
80
|
/** GFM task-list state: `true` for `[x]`/`[X]`, `false` for `[ ]`, absent for plain items. */
|
|
82
81
|
checked?: boolean;
|
|
83
82
|
};
|
|
83
|
+
export type Block = Paragraph | Heading | CodeBlock | List | BlockQuote | HorizontalRule | Table | HTMLBlock | Image | LinkDef | FootnoteDef;
|
|
84
84
|
export type List = {
|
|
85
85
|
type: "list";
|
|
86
86
|
ordered: boolean;
|
package/dist/parsers.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export declare function noneOf(chars: string): Parser<string>;
|
|
|
48
48
|
*/
|
|
49
49
|
export type CharPredicate = (code: number) => boolean;
|
|
50
50
|
/**
|
|
51
|
+
* A faster version of many/ manyWithJoin for character-class scanning.
|
|
52
|
+
*
|
|
51
53
|
* Consume the longest prefix of `input` whose characters all satisfy
|
|
52
54
|
* the predicate (or are contained in the given char-class string). Always
|
|
53
55
|
* succeeds; returns "" when nothing matches.
|
package/dist/parsers.js
CHANGED
|
@@ -126,6 +126,8 @@ function compileCharPredicate(charsOrPred) {
|
|
|
126
126
|
return (code) => code < 128 ? ascii[code] === 1 : set.has(code);
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
|
+
* A faster version of many/ manyWithJoin for character-class scanning.
|
|
130
|
+
*
|
|
129
131
|
* Consume the longest prefix of `input` whose characters all satisfy
|
|
130
132
|
* the predicate (or are contained in the given char-class string). Always
|
|
131
133
|
* succeeds; returns "" when nothing matches.
|