@tanishqkancharla/diffmap 0.1.4 → 0.2.0
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/README.md +69 -61
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +229 -0
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +41 -5
- package/src/errors.ts +10 -0
- package/src/gist/GistApp.tsx +39 -20
- package/src/gist/GistStatus.tsx +2 -51
- package/src/gist/fetchGist.ts +23 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +86 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +156 -0
- package/src/host/home.test.ts +83 -0
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +7 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +87 -0
- package/src/parseViewer.ts +103 -77
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +48 -5
- package/src/share.ts +18 -0
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/FileExcerpt.tsx +25 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +277 -121
- package/src/viewer/TableOfContents.tsx +313 -39
- package/src/viewer/ViewerApp.tsx +120 -68
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +33 -42
- package/src/viewer/viewerMode.ts +1 -1
- package/vite.config.ts +2 -1
- package/vite.gist.config.ts +57 -0
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
package/src/parseFence.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseCallStack, type SourceAnnotation } from "./annotations.js";
|
|
2
|
+
import { WORKING, type Rev } from "./changes.js";
|
|
2
3
|
import {
|
|
3
4
|
parseDiagramAnnotations,
|
|
4
5
|
type DiagramAnnotation,
|
|
@@ -8,7 +9,7 @@ export type Fence =
|
|
|
8
9
|
| { kind: "mermaid"; source: string; annotations: DiagramAnnotation[] }
|
|
9
10
|
| { kind: "html"; source: string }
|
|
10
11
|
| { kind: "callstack"; source: string; lines: SourceAnnotation[] }
|
|
11
|
-
| { kind: "
|
|
12
|
+
| { kind: "changes"; rev: Rev; source: string }
|
|
12
13
|
| { kind: "file"; path: string; start: number; end: number; source: string }
|
|
13
14
|
| { kind: "diff"; path: string | undefined; source: string }
|
|
14
15
|
| { kind: "code"; lang: string; source: string };
|
|
@@ -46,12 +47,11 @@ export function parseFence(lang: string, source: string): Fence {
|
|
|
46
47
|
lines: parseCallStack(trimmed),
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (lang === "changes") {
|
|
51
|
+
const rev = trimmed.trim();
|
|
51
52
|
return {
|
|
52
|
-
kind: "
|
|
53
|
-
|
|
54
|
-
path: sourceDiff[2]!,
|
|
53
|
+
kind: "changes",
|
|
54
|
+
rev: rev.length === 0 ? WORKING : rev,
|
|
55
55
|
source: trimmed,
|
|
56
56
|
};
|
|
57
57
|
}
|
package/src/parseViewer.test.ts
CHANGED
|
@@ -74,6 +74,92 @@ test("parseViewerDocument accepts a spec with valid mermaid", async () => {
|
|
|
74
74
|
);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("parseViewerDocument turns prose [[path#symbol]] into a short reference", () => {
|
|
78
|
+
const source =
|
|
79
|
+
"See [[packages/server/src/agent/Thread.ts#Thread.attach]] here, not `[[path]]` or [x [[a.ts]]](https://x.dev).\n";
|
|
80
|
+
const document = parseViewerDocument(source);
|
|
81
|
+
assert.equal(document instanceof Error, false);
|
|
82
|
+
if (document instanceof Error) return;
|
|
83
|
+
assert.equal(document.references.length, 1);
|
|
84
|
+
const paragraph = document.nodes[0];
|
|
85
|
+
assert.equal(paragraph?.type, "element");
|
|
86
|
+
if (paragraph?.type !== "element") return;
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
paragraph.children.map((node) =>
|
|
89
|
+
node.type === "element"
|
|
90
|
+
? { tag: node.tag, children: node.children }
|
|
91
|
+
: node,
|
|
92
|
+
),
|
|
93
|
+
[
|
|
94
|
+
{ type: "text", value: "See " },
|
|
95
|
+
{
|
|
96
|
+
type: "reference",
|
|
97
|
+
annotation: {
|
|
98
|
+
text: "Thread.ts#Thread.attach",
|
|
99
|
+
references: [
|
|
100
|
+
{
|
|
101
|
+
kind: "file",
|
|
102
|
+
path: "packages/server/src/agent/Thread.ts",
|
|
103
|
+
symbol: "Thread.attach",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ type: "text", value: " here, not " },
|
|
109
|
+
{ tag: "code", children: [{ type: "text", value: "[[path]]" }] },
|
|
110
|
+
{ type: "text", value: " or " },
|
|
111
|
+
{ tag: "a", children: [{ type: "text", value: "x [[a.ts]]" }] },
|
|
112
|
+
{ type: "text", value: "." },
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
assert.equal(
|
|
116
|
+
parseViewerDocument("See [[missing:new:1]].\n") instanceof Error,
|
|
117
|
+
true,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("parseViewerDocument reads commit references and changes fences", () => {
|
|
122
|
+
const document = parseViewerDocument(
|
|
123
|
+
[
|
|
124
|
+
"```changes",
|
|
125
|
+
"a1b2c3d..e4f5a6b",
|
|
126
|
+
"```",
|
|
127
|
+
"",
|
|
128
|
+
"```changes",
|
|
129
|
+
"```",
|
|
130
|
+
"",
|
|
131
|
+
"See [[a1b2c3d:src/store.ts#L12-L30]] and [[src/store.ts#L3]].",
|
|
132
|
+
"",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
);
|
|
135
|
+
assert.equal(document instanceof Error, false);
|
|
136
|
+
if (document instanceof Error) return;
|
|
137
|
+
assert.deepEqual(document.changeRevs, ["a1b2c3d..e4f5a6b", "working"]);
|
|
138
|
+
assert.deepEqual(document.references, [
|
|
139
|
+
{ kind: "file", rev: "a1b2c3d", path: "src/store.ts", start: 12, end: 30 },
|
|
140
|
+
{ kind: "file", path: "src/store.ts", start: 3, end: 3 },
|
|
141
|
+
]);
|
|
142
|
+
// A commit with a symbol, or a changes fence that is not a SHA, is an error.
|
|
143
|
+
assert.equal(
|
|
144
|
+
parseViewerDocument("See [[a1b2c3d:src/store.ts#save]].\n") instanceof
|
|
145
|
+
Error,
|
|
146
|
+
true,
|
|
147
|
+
);
|
|
148
|
+
assert.equal(
|
|
149
|
+
parseViewerDocument("```changes\nmain\n```\n") instanceof Error,
|
|
150
|
+
true,
|
|
151
|
+
);
|
|
152
|
+
// The removed patch fence fails instead of rendering as code.
|
|
153
|
+
assert.match(
|
|
154
|
+
String(
|
|
155
|
+
parseViewerDocument(
|
|
156
|
+
"```source-diff:store:src/store.ts\ndiff --git a/x b/x\n```\n",
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
/source-diff:store:src\/store\.ts" fences were removed/,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
77
163
|
test("parseViewerDocument fails on broken mermaid with file and diagram", () => {
|
|
78
164
|
const source = brokenMermaidSpec();
|
|
79
165
|
const parsed = parseViewerDocument(source, "broken.md");
|
|
@@ -138,6 +224,7 @@ test("startServer does not listen when mermaid is invalid", async () => {
|
|
|
138
224
|
test("shareMarkdownFile does not call gh when mermaid is invalid", async () => {
|
|
139
225
|
await withTempSpec(brokenMermaidSpec(), async (filePath) => {
|
|
140
226
|
const shared = await shareMarkdownFile({
|
|
227
|
+
workspaceRoot: packageRoot,
|
|
141
228
|
filePath,
|
|
142
229
|
isPublic: false,
|
|
143
230
|
});
|
package/src/parseViewer.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import * as errore from "errore";
|
|
2
|
-
import { parsePatchFiles, type CodeViewDiffItem } from "@pierre/diffs";
|
|
3
2
|
import { parseAST } from "md4x/napi";
|
|
4
3
|
import type { ComarkElement, ComarkNode } from "md4x/napi";
|
|
4
|
+
import {
|
|
5
|
+
parseReference,
|
|
6
|
+
referenceLabel,
|
|
7
|
+
type SourceAnnotation,
|
|
8
|
+
type SourceReference,
|
|
9
|
+
} from "./annotations.js";
|
|
10
|
+
import { isRev, type Rev } from "./changes.js";
|
|
5
11
|
import { DiffmapAnnotationError, DiffmapParseError } from "./errors.js";
|
|
6
12
|
import { parseMermaidDiagram } from "./mermaid.js";
|
|
7
13
|
import { parseFence, type Fence } from "./parseFence.js";
|
|
@@ -9,8 +15,10 @@ import { parseFence, type Fence } from "./parseFence.js";
|
|
|
9
15
|
export type ViewerDocument = {
|
|
10
16
|
nodes: ViewerNode[];
|
|
11
17
|
headings: ViewerHeading[];
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
/** Every reference in stacks, diagrams, and prose. */
|
|
19
|
+
references: SourceReference[];
|
|
20
|
+
/** Revs of `changes` fences, in document order. */
|
|
21
|
+
changeRevs: Rev[];
|
|
14
22
|
};
|
|
15
23
|
|
|
16
24
|
export type ViewerHeading = {
|
|
@@ -19,7 +27,12 @@ export type ViewerHeading = {
|
|
|
19
27
|
id: string;
|
|
20
28
|
};
|
|
21
29
|
|
|
22
|
-
export type ViewerNode =
|
|
30
|
+
export type ViewerNode =
|
|
31
|
+
| ViewerText
|
|
32
|
+
| ViewerElement
|
|
33
|
+
| ViewerHtml
|
|
34
|
+
| ViewerView
|
|
35
|
+
| ViewerReference;
|
|
23
36
|
|
|
24
37
|
export type ViewerText = {
|
|
25
38
|
type: "text";
|
|
@@ -44,6 +57,12 @@ export type ViewerView = {
|
|
|
44
57
|
fence: Fence;
|
|
45
58
|
};
|
|
46
59
|
|
|
60
|
+
/** A `[[path#symbol]]` in prose. The annotation text is the short label. */
|
|
61
|
+
export type ViewerReference = {
|
|
62
|
+
type: "reference";
|
|
63
|
+
annotation: SourceAnnotation;
|
|
64
|
+
};
|
|
65
|
+
|
|
47
66
|
export type ViewerElementAttrs = {
|
|
48
67
|
id: string | undefined;
|
|
49
68
|
href: string | undefined;
|
|
@@ -64,49 +83,32 @@ export function parseViewerDocument(source: string, path = "spec") {
|
|
|
64
83
|
catch: (cause) => new DiffmapParseError({ cause }),
|
|
65
84
|
});
|
|
66
85
|
if (tree instanceof Error) return tree;
|
|
67
|
-
const nodes = tree.nodes.flatMap(fromNode);
|
|
86
|
+
const nodes = tree.nodes.flatMap((node) => fromNode(node));
|
|
68
87
|
const fences = collectFences(nodes);
|
|
69
|
-
const
|
|
88
|
+
const changeRevs: Rev[] = [];
|
|
70
89
|
for (const fence of fences) {
|
|
71
|
-
if (fence.kind
|
|
72
|
-
if (sourceDiffs.some((diff) => diff.id === fence.id)) {
|
|
73
|
-
return new DiffmapAnnotationError({
|
|
74
|
-
reason: `Duplicate source diff ID "${fence.id}"`,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
const patches = errore.try({
|
|
78
|
-
try: () => parsePatchFiles(fence.source, undefined, true),
|
|
79
|
-
catch: (cause) =>
|
|
80
|
-
new DiffmapAnnotationError({
|
|
81
|
-
reason: `Invalid patch "${fence.id}"`,
|
|
82
|
-
cause,
|
|
83
|
-
}),
|
|
84
|
-
});
|
|
85
|
-
if (patches instanceof Error) return patches;
|
|
86
|
-
const files = patches.flatMap((patch) => patch.files);
|
|
87
|
-
const fileDiff = files[0];
|
|
88
|
-
if (
|
|
89
|
-
files.length !== 1 ||
|
|
90
|
-
fileDiff === undefined ||
|
|
91
|
-
fileDiff.hunks.length === 0
|
|
92
|
-
) {
|
|
90
|
+
if (fence.kind === "code" && fence.lang.startsWith("source-diff")) {
|
|
93
91
|
return new DiffmapAnnotationError({
|
|
94
|
-
reason: `
|
|
92
|
+
reason: `"${fence.lang}" fences were removed. Commit the change, then use a changes fence with the commit SHA and link lines with [[<sha>:path#L12-L30]]`,
|
|
95
93
|
});
|
|
96
94
|
}
|
|
97
|
-
if (
|
|
95
|
+
if (fence.kind !== "changes") continue;
|
|
96
|
+
if (!isRev(fence.rev)) {
|
|
98
97
|
return new DiffmapAnnotationError({
|
|
99
|
-
reason: `
|
|
98
|
+
reason: `Invalid changes "${fence.rev}". Use a commit SHA, a SHA range (a1b2c3d..e4f5a6b), or leave it empty for uncommitted changes`,
|
|
100
99
|
});
|
|
101
100
|
}
|
|
102
|
-
|
|
101
|
+
changeRevs.push(fence.rev);
|
|
103
102
|
}
|
|
104
|
-
const annotations =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
const annotations = [
|
|
104
|
+
...fences.flatMap((fence) => {
|
|
105
|
+
if (fence.kind === "callstack") return fence.lines;
|
|
106
|
+
if (fence.kind === "mermaid")
|
|
107
|
+
return fence.annotations.map((link) => link.annotation);
|
|
108
|
+
return [];
|
|
109
|
+
}),
|
|
110
|
+
...collectReferences(nodes),
|
|
111
|
+
];
|
|
110
112
|
const mermaidFences = fences.flatMap((fence) =>
|
|
111
113
|
fence.kind === "mermaid" ? [fence] : [],
|
|
112
114
|
);
|
|
@@ -136,44 +138,24 @@ export function parseViewerDocument(source: string, path = "spec") {
|
|
|
136
138
|
for (const line of annotations) {
|
|
137
139
|
if (line.text.includes("[[")) {
|
|
138
140
|
return new DiffmapAnnotationError({
|
|
139
|
-
reason: `Invalid reference in "${line.text}". Use [[
|
|
141
|
+
reason: `Invalid reference in "${line.text}". Use [[path]], [[path#symbol]], [[path#L12-L30]], or [[<commit>:path#L12-L30]]; source-diff patch references were removed`,
|
|
140
142
|
});
|
|
141
143
|
}
|
|
142
144
|
for (const ref of line.references) {
|
|
143
|
-
if (ref.kind
|
|
144
|
-
|
|
145
|
-
ref.start !== undefined &&
|
|
146
|
-
(!Number.isSafeInteger(ref.start) ||
|
|
147
|
-
!Number.isSafeInteger(ref.end) ||
|
|
148
|
-
ref.end! < ref.start)
|
|
149
|
-
) {
|
|
150
|
-
return new DiffmapAnnotationError({
|
|
151
|
-
reason: `Invalid file range in "${ref.path}"`,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
const diff = sourceDiffs.find((item) => item.id === ref.id);
|
|
157
|
-
if (diff === undefined) {
|
|
145
|
+
if (ref.kind !== "file") continue;
|
|
146
|
+
if (ref.rev !== undefined && ref.symbol !== undefined) {
|
|
158
147
|
return new DiffmapAnnotationError({
|
|
159
|
-
reason: `
|
|
148
|
+
reason: `Symbol references read current files. Use a line range with a commit: [[${ref.rev}:${ref.path}#L12-L30]]`,
|
|
160
149
|
});
|
|
161
150
|
}
|
|
162
|
-
const inHunk = diff.fileDiff.hunks.some((hunk) => {
|
|
163
|
-
const start =
|
|
164
|
-
ref.side === "old" ? hunk.deletionStart : hunk.additionStart;
|
|
165
|
-
const count =
|
|
166
|
-
ref.side === "old" ? hunk.deletionCount : hunk.additionCount;
|
|
167
|
-
return ref.start >= start && ref.end < start + count;
|
|
168
|
-
});
|
|
169
151
|
if (
|
|
170
|
-
|
|
171
|
-
!Number.isSafeInteger(ref.
|
|
172
|
-
|
|
173
|
-
|
|
152
|
+
ref.start !== undefined &&
|
|
153
|
+
(!Number.isSafeInteger(ref.start) ||
|
|
154
|
+
!Number.isSafeInteger(ref.end) ||
|
|
155
|
+
ref.end! < ref.start)
|
|
174
156
|
) {
|
|
175
157
|
return new DiffmapAnnotationError({
|
|
176
|
-
reason: `
|
|
158
|
+
reason: `Invalid file range in "${ref.path}"`,
|
|
177
159
|
});
|
|
178
160
|
}
|
|
179
161
|
}
|
|
@@ -181,10 +163,8 @@ export function parseViewerDocument(source: string, path = "spec") {
|
|
|
181
163
|
return {
|
|
182
164
|
nodes,
|
|
183
165
|
headings: collectHeadings(nodes),
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(annotation) => annotation.references.length > 0,
|
|
187
|
-
),
|
|
166
|
+
references: annotations.flatMap((annotation) => annotation.references),
|
|
167
|
+
changeRevs,
|
|
188
168
|
};
|
|
189
169
|
}
|
|
190
170
|
|
|
@@ -196,6 +176,14 @@ function collectFences(nodes: ViewerNode[]): Fence[] {
|
|
|
196
176
|
});
|
|
197
177
|
}
|
|
198
178
|
|
|
179
|
+
function collectReferences(nodes: ViewerNode[]): SourceAnnotation[] {
|
|
180
|
+
return nodes.flatMap((node) => {
|
|
181
|
+
if (node.type === "reference") return [node.annotation];
|
|
182
|
+
if (node.type === "element") return collectReferences(node.children);
|
|
183
|
+
return [];
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
199
187
|
function mermaidWhere(source: string, index: number) {
|
|
200
188
|
const line = mermaidFenceLine(source, index);
|
|
201
189
|
if (line === undefined) return `diagram ${index}`;
|
|
@@ -283,31 +271,67 @@ function uniqueHeadingId(text: string, usedIds: Set<string>) {
|
|
|
283
271
|
|
|
284
272
|
function viewerNodeText(node: ViewerNode): string {
|
|
285
273
|
if (node.type === "text") return node.value;
|
|
274
|
+
if (node.type === "reference") return node.annotation.text;
|
|
286
275
|
if (node.type !== "element") return "";
|
|
287
276
|
return node.children.map(viewerNodeText).join("");
|
|
288
277
|
}
|
|
289
278
|
|
|
290
|
-
function fromNode(node: ComarkNode): ViewerNode[] {
|
|
279
|
+
function fromNode(node: ComarkNode, literal = false): ViewerNode[] {
|
|
291
280
|
if (!Array.isArray(node)) {
|
|
292
|
-
return [{ type: "text", value: node }];
|
|
281
|
+
return literal ? [{ type: "text", value: node }] : proseText(node);
|
|
293
282
|
}
|
|
294
|
-
const converted = fromElement(node);
|
|
283
|
+
const converted = fromElement(node, literal);
|
|
295
284
|
if (converted === undefined) return [];
|
|
296
285
|
if (Array.isArray(converted)) return converted;
|
|
297
286
|
return [converted];
|
|
298
287
|
}
|
|
299
288
|
|
|
289
|
+
/** Splits `[[path#symbol]]` references out of prose text. */
|
|
290
|
+
function proseText(value: string): ViewerNode[] {
|
|
291
|
+
const nodes: ViewerNode[] = [];
|
|
292
|
+
let last = 0;
|
|
293
|
+
for (const match of value.matchAll(/\[\[([^[\]\n]+)\]\]/g)) {
|
|
294
|
+
const reference = parseReference(match[1]!);
|
|
295
|
+
// The removed `[[id:old|new:N]]` patch syntax must fail, not render as text.
|
|
296
|
+
const removed =
|
|
297
|
+
reference === undefined && /^[\w-]+:(old|new):\d/.test(match[1]!);
|
|
298
|
+
if (reference === undefined && !removed) continue;
|
|
299
|
+
if (match.index > last)
|
|
300
|
+
nodes.push({ type: "text", value: value.slice(last, match.index) });
|
|
301
|
+
nodes.push({
|
|
302
|
+
type: "reference",
|
|
303
|
+
annotation:
|
|
304
|
+
reference === undefined
|
|
305
|
+
? { text: match[0], references: [] }
|
|
306
|
+
: { text: referenceLabel(reference, true), references: [reference] },
|
|
307
|
+
});
|
|
308
|
+
last = match.index + match[0].length;
|
|
309
|
+
}
|
|
310
|
+
if (last < value.length)
|
|
311
|
+
nodes.push({ type: "text", value: value.slice(last) });
|
|
312
|
+
return nodes;
|
|
313
|
+
}
|
|
314
|
+
|
|
300
315
|
function fromElement(
|
|
301
316
|
element: ComarkElement,
|
|
317
|
+
literal: boolean,
|
|
302
318
|
): ViewerNode | ViewerNode[] | undefined {
|
|
303
319
|
const [tag, , ...children] = element;
|
|
304
320
|
if (tag === null) return undefined;
|
|
305
|
-
|
|
321
|
+
// Code spans show the syntax literally; links cannot nest a button.
|
|
322
|
+
const literalChildren = literal || tag === "code" || tag === "a";
|
|
323
|
+
if (tag === "template")
|
|
324
|
+
return children.flatMap((child) => fromNode(child, literalChildren));
|
|
306
325
|
if (tag === "pre") {
|
|
307
326
|
return fenceView(stringAttr(element, "language"), comarkText(children));
|
|
308
327
|
}
|
|
309
328
|
if (tag === "mermaid") return fenceView("mermaid", comarkText(children));
|
|
310
329
|
if (tag === "callstack") return fenceView("callstack", comarkText(children));
|
|
330
|
+
if (tag === "changes")
|
|
331
|
+
return fenceView(
|
|
332
|
+
"changes",
|
|
333
|
+
stringAttr(element, "rev") ?? comarkText(children),
|
|
334
|
+
);
|
|
311
335
|
if (tag === "diff") {
|
|
312
336
|
const path = stringAttr(element, "path");
|
|
313
337
|
const lang = path === undefined ? "diff" : `diff:${path}`;
|
|
@@ -315,7 +339,9 @@ function fromElement(
|
|
|
315
339
|
}
|
|
316
340
|
if (tag === "file") return fileView(element, children);
|
|
317
341
|
if (tag === "html") return htmlView(element, children);
|
|
318
|
-
const convertedChildren = children.flatMap(
|
|
342
|
+
const convertedChildren = children.flatMap((child) =>
|
|
343
|
+
fromNode(child, literalChildren),
|
|
344
|
+
);
|
|
319
345
|
return {
|
|
320
346
|
type: "element",
|
|
321
347
|
tag,
|
|
@@ -352,7 +378,7 @@ function fileView(
|
|
|
352
378
|
children: ComarkNode[],
|
|
353
379
|
): ViewerView | ViewerNode[] {
|
|
354
380
|
const path = stringAttr(element, "path");
|
|
355
|
-
if (path === undefined) return children.flatMap(fromNode);
|
|
381
|
+
if (path === undefined) return children.flatMap((child) => fromNode(child));
|
|
356
382
|
const start = lineAttr(element, "start");
|
|
357
383
|
const end = lineAttr(element, "end");
|
|
358
384
|
// MDC ::file with no range loads the whole file.
|