@cueloop/schema 0.1.0-alpha.9 → 0.1.0-alpha.90
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/package.json +17 -17
- package/src/anchor.test.ts +340 -40
- package/src/anchor.ts +378 -43
- package/src/diff.test.ts +42 -25
- package/src/diff.ts +112 -77
- package/src/feedback.test.ts +318 -41
- package/src/feedback.ts +224 -54
- package/src/fuzzy.test.ts +110 -0
- package/src/fuzzy.ts +132 -0
- package/src/herdr.test.ts +50 -0
- package/src/herdr.ts +54 -0
- package/src/history.test.ts +401 -0
- package/src/history.ts +425 -0
- package/src/index.ts +9 -0
- package/src/inline.test.ts +195 -0
- package/src/inline.ts +240 -0
- package/src/markdown.test.ts +138 -22
- package/src/markdown.ts +333 -84
- package/src/participants.test.ts +105 -0
- package/src/participants.ts +51 -0
- package/src/path-view.test.ts +206 -0
- package/src/path-view.ts +90 -0
- package/src/share-access.test.ts +20 -0
- package/src/share-access.ts +13 -0
- package/src/share-links.test.ts +63 -0
- package/src/share-links.ts +29 -0
- package/src/thread-surface.test.ts +6 -0
- package/src/thread-surface.ts +6 -0
- package/src/types.test.ts +58 -5
- package/src/types.ts +341 -24
- package/src/working-copy.test.ts +209 -24
- package/src/working-copy.ts +148 -12
package/src/inline.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline markdown tokenizer: turns one block's text into styled runs while
|
|
3
|
+
* preserving exact source offsets, so the review surface can conceal the markup
|
|
4
|
+
* markers yet keep quote anchors char-precise.
|
|
5
|
+
*
|
|
6
|
+
* Each run is either VISIBLE text - carrying `start`, its offset in the input -
|
|
7
|
+
* or a CONCEALED marker (`**`, `` ` ``, `[`, `](url)`, ...) with `start: null`.
|
|
8
|
+
* A null run occupies no source offset, exactly like a word-diff removal, so the
|
|
9
|
+
* renderer's offset-mapping (overlayMarks / renderedOffsetFor / workRangeForRendered)
|
|
10
|
+
* paints highlights across the visible letters and skips the markers unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Pure and dependency-free: the parse tree never escapes this module - the only
|
|
13
|
+
* output is the flat run list. Covers the plan subset: **strong**, *em*, `code`,
|
|
14
|
+
* ~~strike~~, [label](url), and backslash escapes. Unbalanced markup falls back
|
|
15
|
+
* to literal text.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** What a visible run represents; a marker run is concealed markup, no emphasis. */
|
|
19
|
+
export type InlineRole = "text" | "strong" | "em" | "code" | "strike" | "link" | "marker";
|
|
20
|
+
|
|
21
|
+
export interface InlineRun {
|
|
22
|
+
text: string;
|
|
23
|
+
role: InlineRole;
|
|
24
|
+
/** Offset of this run in the input text; null for a concealed marker. */
|
|
25
|
+
start: number | null;
|
|
26
|
+
/** Link target, present only on `link` runs. */
|
|
27
|
+
href?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An emphasis span opened by a delimiter at some index, once its close is found. */
|
|
31
|
+
interface Span {
|
|
32
|
+
/** Length of the opening marker (e.g. 2 for `**`). */
|
|
33
|
+
openLength: number;
|
|
34
|
+
/** Length of the closing marker. */
|
|
35
|
+
closeLength: number;
|
|
36
|
+
/** Index in `text` where the closing marker starts. */
|
|
37
|
+
closeIndex: number;
|
|
38
|
+
/** Emphasis role applied to the span's (recursively parsed) content. */
|
|
39
|
+
role: "strong" | "em" | "strike";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PUNCTUATION = new Set(["*", "_", "`", "~", "[", "]", "(", ")", "\\"]);
|
|
43
|
+
|
|
44
|
+
/** Tokenize one block's text into visible + concealed-marker runs, in order. */
|
|
45
|
+
export function inlineRuns(text: string): InlineRun[] {
|
|
46
|
+
const runs: InlineRun[] = [];
|
|
47
|
+
|
|
48
|
+
tokenize(text, 0, "text", runs);
|
|
49
|
+
|
|
50
|
+
return runs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Walk `source` (whose first character sits at absolute offset `base`), emitting
|
|
55
|
+
* plain text as `role` runs and recursing into emphasis spans. Links and code
|
|
56
|
+
* spans are handled inline because their content is not further emphasized the
|
|
57
|
+
* same way.
|
|
58
|
+
*/
|
|
59
|
+
function tokenize(source: string, base: number, role: InlineRole, out: InlineRun[]): void {
|
|
60
|
+
let plainStart = 0;
|
|
61
|
+
let cursor = 0;
|
|
62
|
+
const flushPlain = (end: number): void => {
|
|
63
|
+
if (end > plainStart) {
|
|
64
|
+
out.push({ text: source.slice(plainStart, end), role, start: base + plainStart });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
while (cursor < source.length) {
|
|
69
|
+
const char = source[cursor]!;
|
|
70
|
+
|
|
71
|
+
if (char === "\\" && cursor + 1 < source.length && PUNCTUATION.has(source[cursor + 1]!)) {
|
|
72
|
+
flushPlain(cursor);
|
|
73
|
+
out.push({ text: "\\", role: "marker", start: null });
|
|
74
|
+
out.push({ text: source[cursor + 1]!, role, start: base + cursor + 1 });
|
|
75
|
+
cursor += 2;
|
|
76
|
+
plainStart = cursor;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const link = matchLink(source, cursor);
|
|
81
|
+
|
|
82
|
+
if (link) {
|
|
83
|
+
flushPlain(cursor);
|
|
84
|
+
out.push({ text: source.slice(cursor, link.labelStart), role: "marker", start: null });
|
|
85
|
+
out.push({
|
|
86
|
+
text: source.slice(link.labelStart, link.labelEnd),
|
|
87
|
+
role: "link",
|
|
88
|
+
start: base + link.labelStart,
|
|
89
|
+
href: source.slice(link.hrefStart, link.hrefEnd),
|
|
90
|
+
});
|
|
91
|
+
out.push({ text: source.slice(link.labelEnd, link.end), role: "marker", start: null });
|
|
92
|
+
cursor = link.end;
|
|
93
|
+
plainStart = cursor;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const code = matchCode(source, cursor);
|
|
98
|
+
|
|
99
|
+
if (code) {
|
|
100
|
+
flushPlain(cursor);
|
|
101
|
+
out.push({ text: source.slice(cursor, code.contentStart), role: "marker", start: null });
|
|
102
|
+
out.push({
|
|
103
|
+
text: source.slice(code.contentStart, code.contentEnd),
|
|
104
|
+
role: "code",
|
|
105
|
+
start: base + code.contentStart,
|
|
106
|
+
});
|
|
107
|
+
out.push({ text: source.slice(code.contentEnd, code.end), role: "marker", start: null });
|
|
108
|
+
cursor = code.end;
|
|
109
|
+
plainStart = cursor;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const span = matchEmphasis(source, cursor);
|
|
114
|
+
|
|
115
|
+
if (span) {
|
|
116
|
+
flushPlain(cursor);
|
|
117
|
+
out.push({
|
|
118
|
+
text: source.slice(cursor, cursor + span.openLength),
|
|
119
|
+
role: "marker",
|
|
120
|
+
start: null,
|
|
121
|
+
});
|
|
122
|
+
const contentStart = cursor + span.openLength;
|
|
123
|
+
|
|
124
|
+
tokenize(source.slice(contentStart, span.closeIndex), base + contentStart, span.role, out);
|
|
125
|
+
out.push({
|
|
126
|
+
text: source.slice(span.closeIndex, span.closeIndex + span.closeLength),
|
|
127
|
+
role: "marker",
|
|
128
|
+
start: null,
|
|
129
|
+
});
|
|
130
|
+
cursor = span.closeIndex + span.closeLength;
|
|
131
|
+
plainStart = cursor;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
cursor++;
|
|
136
|
+
}
|
|
137
|
+
flushPlain(source.length);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A `[label](url)` link at `index`, or null. Empty label or url does not match. */
|
|
141
|
+
function matchLink(
|
|
142
|
+
text: string,
|
|
143
|
+
index: number,
|
|
144
|
+
): {
|
|
145
|
+
labelStart: number;
|
|
146
|
+
labelEnd: number;
|
|
147
|
+
hrefStart: number;
|
|
148
|
+
hrefEnd: number;
|
|
149
|
+
end: number;
|
|
150
|
+
} | null {
|
|
151
|
+
if (text[index] !== "[") return null;
|
|
152
|
+
const labelStart = index + 1;
|
|
153
|
+
const labelEnd = text.indexOf("]", labelStart);
|
|
154
|
+
|
|
155
|
+
if (labelEnd < 0 || labelEnd === labelStart) return null;
|
|
156
|
+
if (text[labelEnd + 1] !== "(") return null;
|
|
157
|
+
const hrefStart = labelEnd + 2;
|
|
158
|
+
// the destination may carry balanced parentheses (wiki URLs); the span ends
|
|
159
|
+
// at the first unmatched closer
|
|
160
|
+
let depth = 0;
|
|
161
|
+
|
|
162
|
+
for (let scan = hrefStart; scan < text.length; scan++) {
|
|
163
|
+
const character = text[scan];
|
|
164
|
+
|
|
165
|
+
if (character === "(") depth++;
|
|
166
|
+
else if (character === ")") {
|
|
167
|
+
if (depth > 0) {
|
|
168
|
+
depth--;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (scan === hrefStart) return null;
|
|
172
|
+
|
|
173
|
+
return { labelStart, labelEnd, hrefStart, hrefEnd: scan, end: scan + 1 };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A `` `code` `` span at `index` (n backticks .. n backticks), or null. */
|
|
181
|
+
function matchCode(
|
|
182
|
+
text: string,
|
|
183
|
+
index: number,
|
|
184
|
+
): { contentStart: number; contentEnd: number; end: number } | null {
|
|
185
|
+
if (text[index] !== "`") return null;
|
|
186
|
+
let fence = 0;
|
|
187
|
+
|
|
188
|
+
while (text[index + fence] === "`") fence++;
|
|
189
|
+
const contentStart = index + fence;
|
|
190
|
+
const closer = "`".repeat(fence);
|
|
191
|
+
const contentEnd = text.indexOf(closer, contentStart);
|
|
192
|
+
|
|
193
|
+
if (contentEnd < 0 || contentEnd === contentStart) return null;
|
|
194
|
+
|
|
195
|
+
return { contentStart, contentEnd, end: contentEnd + fence };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A `**strong**`, `*em*`, or `~~strike~~` span at `index`, or null. */
|
|
199
|
+
function matchEmphasis(text: string, index: number): Span | null {
|
|
200
|
+
const two = text.slice(index, index + 2);
|
|
201
|
+
|
|
202
|
+
if (two === "**") return closingSpan(text, index, 2, "strong");
|
|
203
|
+
if (two === "~~") return closingSpan(text, index, 2, "strike");
|
|
204
|
+
if (text[index] === "*") return closingSpan(text, index, 1, "em");
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Find the matching close for a marker of `length` at `index`; null if unbalanced. */
|
|
210
|
+
function closingSpan(
|
|
211
|
+
text: string,
|
|
212
|
+
index: number,
|
|
213
|
+
length: number,
|
|
214
|
+
role: "strong" | "em" | "strike",
|
|
215
|
+
): Span | null {
|
|
216
|
+
const marker = text.slice(index, index + length);
|
|
217
|
+
const contentStart = index + length;
|
|
218
|
+
const closeIndex =
|
|
219
|
+
length === 1 ? findLoneStar(text, contentStart) : text.indexOf(marker, contentStart);
|
|
220
|
+
|
|
221
|
+
if (closeIndex < 0 || closeIndex === contentStart) return null; // unbalanced or empty
|
|
222
|
+
|
|
223
|
+
return { openLength: length, closeLength: length, closeIndex, role };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Index of the next standalone `*` (not part of a `**` run), or -1. */
|
|
227
|
+
function findLoneStar(text: string, from: number): number {
|
|
228
|
+
let index = from;
|
|
229
|
+
|
|
230
|
+
while (index < text.length) {
|
|
231
|
+
if (text[index] === "*") {
|
|
232
|
+
if (text[index + 1] !== "*") return index;
|
|
233
|
+
while (text[index] === "*") index++; // skip the whole `**` run
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
index++;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
package/src/markdown.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { parseBlocks, blockToMd, sectionOf } from "./markdown";
|
|
2
|
+
import { parseBlocks, blockToMd, sectionOf, stripLeadingBlockMarker } from "./markdown";
|
|
3
3
|
|
|
4
4
|
const SAMPLE = `# Title
|
|
5
5
|
|
|
@@ -29,8 +29,11 @@ const y = 2;
|
|
|
29
29
|
|
|
30
30
|
describe("parseBlocks", () => {
|
|
31
31
|
test("parses every block kind with source line ranges", () => {
|
|
32
|
+
// Act
|
|
32
33
|
const blocks = parseBlocks(SAMPLE);
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(blocks.map((block) => block.kind)).toEqual([
|
|
34
37
|
"h1",
|
|
35
38
|
"h2",
|
|
36
39
|
"p",
|
|
@@ -43,7 +46,8 @@ describe("parseBlocks", () => {
|
|
|
43
46
|
"quote",
|
|
44
47
|
"hr",
|
|
45
48
|
]);
|
|
46
|
-
const code = blocks.find((
|
|
49
|
+
const code = blocks.find((block) => block.kind === "code")!;
|
|
50
|
+
|
|
47
51
|
expect(code.lang).toBe("ts");
|
|
48
52
|
expect(code.text).toBe("const x = 1;\nconst y = 2;");
|
|
49
53
|
// fence lines included in the source range
|
|
@@ -51,50 +55,162 @@ describe("parseBlocks", () => {
|
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
test("multi-line paragraph keeps its text and range", () => {
|
|
58
|
+
// Arrange
|
|
54
59
|
const blocks = parseBlocks(SAMPLE);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const paragraph = blocks.find((block) => block.kind === "p")!;
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(paragraph.text).toBe("A paragraph that\nspans two lines.");
|
|
64
|
+
expect(paragraph.lineEnd - paragraph.lineStart).toBe(1);
|
|
58
65
|
});
|
|
59
66
|
|
|
60
67
|
test("line ranges index into the source", () => {
|
|
68
|
+
// Arrange
|
|
61
69
|
const lines = SAMPLE.split("\n");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(lines[
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
for (const block of parseBlocks(SAMPLE)) {
|
|
73
|
+
if (block.kind === "code") {
|
|
74
|
+
expect(lines[block.lineStart]!.startsWith("```")).toBe(true);
|
|
75
|
+
} else if (block.kind === "h2") {
|
|
76
|
+
expect(lines[block.lineStart]!.startsWith("## ")).toBe(true);
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
});
|
|
70
80
|
|
|
71
81
|
test("round-trips through blockToMd", () => {
|
|
82
|
+
// Arrange
|
|
72
83
|
const blocks = parseBlocks(SAMPLE);
|
|
73
|
-
let
|
|
84
|
+
let orderedItemCount = 0;
|
|
74
85
|
const rebuilt = blocks
|
|
75
|
-
.map((
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
.map((block) => {
|
|
87
|
+
orderedItemCount = block.kind === "oli" ? orderedItemCount + 1 : 0;
|
|
88
|
+
|
|
89
|
+
return blockToMd(block, orderedItemCount || 1);
|
|
78
90
|
})
|
|
79
91
|
.join("\n\n");
|
|
92
|
+
|
|
93
|
+
// Act
|
|
80
94
|
// re-parsing the rebuild yields the same kinds and texts
|
|
81
95
|
const again = parseBlocks(rebuilt);
|
|
82
|
-
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(again.map((block) => [block.kind, block.text])).toEqual(
|
|
99
|
+
blocks.map((block) => [block.kind, block.text]),
|
|
100
|
+
);
|
|
83
101
|
});
|
|
84
102
|
|
|
85
103
|
test("unknown constructs degrade to paragraphs, no content lost", () => {
|
|
86
|
-
|
|
104
|
+
// Arrange
|
|
105
|
+
const md = "<div>raw html</div>";
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
const blocks = parseBlocks(md);
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
expect(blocks.map((block) => block.kind)).toEqual(["p"]);
|
|
112
|
+
expect(blocks[0]!.text).toBe("<div>raw html</div>");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a GFM table parses to one table block that keeps its source verbatim", () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const md = "before\n\n| a | b |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n\nafter";
|
|
118
|
+
|
|
119
|
+
// Act
|
|
87
120
|
const blocks = parseBlocks(md);
|
|
88
|
-
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(blocks.map((block) => block.kind)).toEqual(["p", "table", "p"]);
|
|
124
|
+
const table = blocks.find((block) => block.kind === "table")!;
|
|
125
|
+
|
|
126
|
+
expect(table.text).toBe("| a | b |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a single-column table and an aligned delimiter still parse as a table", () => {
|
|
130
|
+
// Assert
|
|
131
|
+
expect(parseBlocks("| only |\n| --- |\n| one |")[0]!.kind).toBe("table");
|
|
132
|
+
expect(parseBlocks("| l | r |\n| :--- | ---: |\n| a | b |")[0]!.kind).toBe("table");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a plain rule line is an hr, never a table", () => {
|
|
136
|
+
// Assert
|
|
137
|
+
expect(parseBlocks("above\n\n---\n\nbelow").map((block) => block.kind)).toEqual([
|
|
138
|
+
"p",
|
|
139
|
+
"hr",
|
|
140
|
+
"p",
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("leading frontmatter parses to a frontmatter block, closed by its fence", () => {
|
|
145
|
+
// Arrange
|
|
146
|
+
const md = "---\ntitle: Plan\nowner: me\n---\n\n# Heading\n";
|
|
147
|
+
|
|
148
|
+
// Act
|
|
149
|
+
const blocks = parseBlocks(md);
|
|
150
|
+
|
|
151
|
+
// Assert
|
|
152
|
+
expect(blocks.map((block) => block.kind)).toEqual(["frontmatter", "h1"]);
|
|
153
|
+
expect(blocks[0]!.text).toBe("title: Plan\nowner: me");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a leading fence with no closing fence stays an hr, not frontmatter", () => {
|
|
157
|
+
// Assert
|
|
158
|
+
expect(parseBlocks("---\njust a paragraph\n").map((block) => block.kind)).toEqual(["hr", "p"]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("table and frontmatter round-trip through blockToMd", () => {
|
|
162
|
+
// Arrange
|
|
163
|
+
const md = "---\ntitle: Plan\n---\n\n| a | b |\n|---|---|\n| 1 | 2 |";
|
|
164
|
+
const blocks = parseBlocks(md);
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const rebuilt = blocks.map((block) => blockToMd(block)).join("\n\n");
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect(parseBlocks(rebuilt).map((block) => [block.kind, block.text])).toEqual(
|
|
171
|
+
blocks.map((block) => [block.kind, block.text]),
|
|
172
|
+
);
|
|
89
173
|
});
|
|
90
174
|
});
|
|
91
175
|
|
|
92
176
|
describe("sectionOf", () => {
|
|
93
177
|
test("returns the nearest preceding heading", () => {
|
|
178
|
+
// Arrange
|
|
94
179
|
const blocks = parseBlocks(SAMPLE);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
180
|
+
|
|
181
|
+
// Assert
|
|
182
|
+
const orderedItemIndex = blocks.findIndex((block) => block.kind === "oli");
|
|
183
|
+
|
|
184
|
+
expect(sectionOf(blocks, orderedItemIndex)).toBe("Steps");
|
|
185
|
+
const paragraphIndex = blocks.findIndex((block) => block.kind === "p");
|
|
186
|
+
|
|
187
|
+
expect(sectionOf(blocks, paragraphIndex)).toBe("Context");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("stripLeadingBlockMarker", () => {
|
|
192
|
+
test("strips the marker the parser strips for every marked block kind", () => {
|
|
193
|
+
// Assert
|
|
194
|
+
expect(stripLeadingBlockMarker("# Heading")).toBe("Heading");
|
|
195
|
+
expect(stripLeadingBlockMarker("### Heading")).toBe("Heading");
|
|
196
|
+
expect(stripLeadingBlockMarker("- bullet item")).toBe("bullet item");
|
|
197
|
+
expect(stripLeadingBlockMarker("3. ordered item")).toBe("ordered item");
|
|
198
|
+
expect(stripLeadingBlockMarker("> quoted line")).toBe("quoted line");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("agrees with the text the parser produces", () => {
|
|
202
|
+
// Arrange
|
|
203
|
+
const source = "- This is a dummy plan - confirm the item";
|
|
204
|
+
|
|
205
|
+
// Act
|
|
206
|
+
const [listBlock] = parseBlocks(source);
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
209
|
+
expect(stripLeadingBlockMarker(source)).toBe(listBlock!.text);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("leaves an unmarked line untouched", () => {
|
|
213
|
+
// Assert
|
|
214
|
+
expect(stripLeadingBlockMarker("a plain paragraph")).toBe("a plain paragraph");
|
|
99
215
|
});
|
|
100
216
|
});
|