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.
@@ -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,
@@ -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 }) => ({ ord: true, start: parseInt(digits, 10) }));
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
- const itemHeadOf = (indent, markerParser) => map(seqC(indentOf(indent), capture(markerParser, "marker"), char(" "), capture(optional(taskCheckbox), "checked"), capture(manyTillStr("\n"), "line"), or(char("\n"), eof)), ({ marker, checked, line }) => {
86
- const raw = { marker, line };
87
- if (checked !== null)
88
- raw.checked = checked;
89
- return raw;
90
- });
91
- const parseInline = (line) => {
92
- const inline = many1(inlineMarkdownParser)(line);
93
- return inline.success ? inline.result : [];
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
- // One list item: an item-head followed by an optional sublist at +2 indent.
96
- const itemWithSublist = (indent, markerParser) => map(seqC(capture(itemHeadOf(indent, markerParser), "raw"), capture(optional(lazy(() => listParserAt(indent + 2))), "sublist")), ({ raw, sublist }) => {
97
- const item = { content: parseInline(raw.line) };
98
- if (sublist)
99
- item.sublist = sublist;
100
- if (raw.checked !== undefined)
101
- item.checked = raw.checked;
102
- return { marker: raw.marker, item };
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
- // A list of one or more items that all share a marker family.
105
- const listOf = (indent, markerParser) => map(seqC(capture(itemWithSublist(indent, markerParser), "first"), capture(many(itemWithSublist(indent, markerParser)), "rest")), ({ first, rest }) => ({
106
- type: "list",
107
- ordered: first.marker.ord,
108
- start: first.marker.start,
109
- items: [first.item, ...rest.map((r) => r.item)],
110
- }));
111
- const listParserAt = (indent) => or(listOf(indent, unorderedMarker), listOf(indent, orderedMarker));
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, or, optional, many, capture, seqC, map } from "../../combinators.js";
7
- import { spaces, newline } from "../../parsers.js";
8
- import { headingParser, codeBlockParser, blockQuoteParser, paragraphParser, imageParser, horizontalRuleParser, setextHeadingParser, indentedCodeBlockParser, listParser, tableParser, htmlBlockParser, } from "./blocks.js";
9
- import { linkDefinitionParser, footnoteDefinitionParser, resolveReferences, } from "./references.js";
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: InlineMarkdown[];
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tarsec",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "A parser combinator library for TypeScript, inspired by Parsec.",
5
5
  "homepage": "https://github.com/egonSchiele/tarsec",
6
6
  "scripts": {