@suzumiyaaoba/mdxr 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- package/src/ui/waterfall.tsx +142 -0
package/src/ui/deps.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { TITLE_PROP } from "./attrs.js";
|
|
5
|
+
import { ListPanel, ListRow, RowIcon, RowNote, Tag } from "./bits.js";
|
|
6
|
+
import { MONO_CLS, TONE_TEXT } from "./tones.js";
|
|
7
|
+
|
|
8
|
+
export const DEP_KINDS = [
|
|
9
|
+
"calls",
|
|
10
|
+
"extends",
|
|
11
|
+
"implements",
|
|
12
|
+
"imports",
|
|
13
|
+
"reads",
|
|
14
|
+
"writes",
|
|
15
|
+
] as const;
|
|
16
|
+
export type DepKind = (typeof DEP_KINDS)[number];
|
|
17
|
+
|
|
18
|
+
const KINDS: Record<DepKind, { cls: string; icon: string }> = {
|
|
19
|
+
calls: {
|
|
20
|
+
cls: TONE_TEXT.sky,
|
|
21
|
+
icon: "lucide:square-function",
|
|
22
|
+
},
|
|
23
|
+
extends: {
|
|
24
|
+
cls: TONE_TEXT.violet,
|
|
25
|
+
icon: "lucide:git-branch",
|
|
26
|
+
},
|
|
27
|
+
implements: {
|
|
28
|
+
cls: TONE_TEXT.teal,
|
|
29
|
+
icon: "lucide:layers",
|
|
30
|
+
},
|
|
31
|
+
imports: {
|
|
32
|
+
cls: TONE_TEXT.neutral,
|
|
33
|
+
icon: "lucide:package",
|
|
34
|
+
},
|
|
35
|
+
reads: {
|
|
36
|
+
cls: TONE_TEXT.neutral,
|
|
37
|
+
icon: "lucide:eye",
|
|
38
|
+
},
|
|
39
|
+
writes: {
|
|
40
|
+
cls: TONE_TEXT.amber,
|
|
41
|
+
icon: "lucide:pencil",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Deps = defineComponent(
|
|
46
|
+
{
|
|
47
|
+
description:
|
|
48
|
+
"依存関係エッジ一覧のコンテナ。<Dep> を並べる。title でキャプションバー",
|
|
49
|
+
schema: v.looseObject(TITLE_PROP),
|
|
50
|
+
},
|
|
51
|
+
({ title, children }) => <ListPanel title={title}>{children}</ListPanel>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const Dep = defineComponent(
|
|
55
|
+
{
|
|
56
|
+
description:
|
|
57
|
+
"依存エッジ1行。from → to。kind は imports|calls|extends|implements|reads|writes。children は注記",
|
|
58
|
+
schema: v.looseObject({
|
|
59
|
+
from: v.string(),
|
|
60
|
+
kind: v.optional(v.picklist(DEP_KINDS), "imports"),
|
|
61
|
+
to: v.string(),
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
({ from, to, kind, children }) => {
|
|
65
|
+
const k = KINDS[kind];
|
|
66
|
+
return (
|
|
67
|
+
<ListRow gapX="2">
|
|
68
|
+
<code className={MONO_CLS}>{from}</code>
|
|
69
|
+
<RowIcon name="lucide:arrow-right" />
|
|
70
|
+
<code className={MONO_CLS}>{to}</code>
|
|
71
|
+
<Tag className={k.cls} icon={k.icon}>
|
|
72
|
+
{kind}
|
|
73
|
+
</Tag>
|
|
74
|
+
<RowNote>{children}</RowNote>
|
|
75
|
+
</ListRow>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { attrTrue, BOOLISH_PROP } from "./attrs.js";
|
|
5
|
+
import { Icon } from "./icon.js";
|
|
6
|
+
import { BORDER_CLS, TEXT, TRIM_CLS } from "./tones.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collapsible section on native `<details>` — opens/closes without client
|
|
10
|
+
* JS, unlike the shadcn Collapsible (which renders its initial state only).
|
|
11
|
+
*/
|
|
12
|
+
export const Details = defineComponent(
|
|
13
|
+
{
|
|
14
|
+
description:
|
|
15
|
+
'折りたたみセクション (ネイティブ <details> — JS なしで開閉)。summary="..." で要約、open で初期展開',
|
|
16
|
+
schema: v.looseObject({
|
|
17
|
+
open: BOOLISH_PROP,
|
|
18
|
+
summary: v.optional(v.string()),
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
({ summary, open, children }) => {
|
|
22
|
+
const isOpen = attrTrue(open);
|
|
23
|
+
return (
|
|
24
|
+
<details
|
|
25
|
+
className={`mdxr-details my-6 rounded-lg border ${BORDER_CLS}`}
|
|
26
|
+
open={isOpen}
|
|
27
|
+
>
|
|
28
|
+
<summary
|
|
29
|
+
className={`flex cursor-pointer items-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium ${TEXT.chip} transition-colors select-none hover:bg-neutral-50 active:bg-neutral-100 dark:hover:bg-neutral-900 dark:active:bg-neutral-800`}
|
|
30
|
+
>
|
|
31
|
+
<Icon
|
|
32
|
+
className={`mdxr-chev h-4 w-4 shrink-0 ${TEXT.faint}`}
|
|
33
|
+
name="lucide:chevron-right"
|
|
34
|
+
/>
|
|
35
|
+
{summary ?? "Details"}
|
|
36
|
+
</summary>
|
|
37
|
+
<div className={`border-t px-4 py-3 ${BORDER_CLS} ${TRIM_CLS}`}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
</details>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified-diff parsing. `parseDiff` turns patch text into per-file
|
|
3
|
+
* structures; `DiffView` (diff.tsx) renders them. Bare `+`/`-` line
|
|
4
|
+
* streams (no `diff --git`/`@@` headers) parse as a single anonymous file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface DiffRow {
|
|
8
|
+
kind: "add" | "ctx" | "del" | "note";
|
|
9
|
+
newLine?: number;
|
|
10
|
+
oldLine?: number;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DiffHunk {
|
|
15
|
+
header?: string;
|
|
16
|
+
rows: DiffRow[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FileDiff {
|
|
20
|
+
adds: number;
|
|
21
|
+
dels: number;
|
|
22
|
+
hunks: DiffHunk[];
|
|
23
|
+
meta: string[];
|
|
24
|
+
newPath?: string;
|
|
25
|
+
oldPath?: string;
|
|
26
|
+
raw: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One highlighted token inside a rendered diff row. */
|
|
30
|
+
export interface DiffHlToken {
|
|
31
|
+
/** Shiki style string — `--shiki-light`/`--shiki-dark` vars. */
|
|
32
|
+
s?: string;
|
|
33
|
+
/** Token text. */
|
|
34
|
+
t: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-file highlight payload parked on `<code data-diffhl>` by rehypeShiki:
|
|
39
|
+
* `[file][hunk][row]` → token list. `null` means "render the row's plain
|
|
40
|
+
* text" — note rows, or files whose language could not be resolved.
|
|
41
|
+
*/
|
|
42
|
+
export type DiffHl = (DiffHlToken[] | null)[][][];
|
|
43
|
+
|
|
44
|
+
/** JSON from the fence's `data-diffhl` attribute, or undefined when absent or
|
|
45
|
+
* malformed — malformed data degrades to unhighlighted rows, never a crash. */
|
|
46
|
+
export const parseDiffHl = (raw: string | undefined): DiffHl | undefined => {
|
|
47
|
+
if (raw === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const parsed: unknown = JSON.parse(raw);
|
|
52
|
+
return Array.isArray(parsed) ? (parsed as DiffHl) : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const GIT_RE =
|
|
59
|
+
/^diff --git (?:"(?<aq>[^"]+)" "(?<bq>[^"]+)"|(?<a>\S+) (?<b>\S+))/u;
|
|
60
|
+
const OLD_RE = /^--- (?:"(?<q>[^"]+)"|(?<p>\S+))/u;
|
|
61
|
+
const NEW_RE = /^\+\+\+ (?:"(?<q>[^"]+)"|(?<p>\S+))/u;
|
|
62
|
+
const HUNK_RE =
|
|
63
|
+
/^@@ -(?<o>\d+)(?:,(?<oc>\d+))? \+(?<n>\d+)(?:,(?<nc>\d+))? @@/u;
|
|
64
|
+
const RENAME_FROM_RE = /^rename from (?<p>.+)/u;
|
|
65
|
+
const RENAME_TO_RE = /^rename to (?<p>.+)/u;
|
|
66
|
+
|
|
67
|
+
/** `a/src/x.ts` → `src/x.ts`; quoted names unquoted; `/dev/null` → undefined. */
|
|
68
|
+
const cleanPath = (p: string | undefined): string | undefined => {
|
|
69
|
+
if (p === undefined) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const unq = p.replaceAll(/^"|"$/gu, "").replace(/^[ab]\//u, "");
|
|
73
|
+
return unq === "/dev/null" || unq === "" ? undefined : unq;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const newFile = (): FileDiff => ({
|
|
77
|
+
adds: 0,
|
|
78
|
+
dels: 0,
|
|
79
|
+
hunks: [],
|
|
80
|
+
meta: [],
|
|
81
|
+
raw: [],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
interface ParseState {
|
|
85
|
+
cur?: FileDiff;
|
|
86
|
+
files: FileDiff[];
|
|
87
|
+
hunk?: DiffHunk;
|
|
88
|
+
/** Rows the declared hunk still expects on each side (Infinity for implicit
|
|
89
|
+
* hunks — bare +/- streams have no header counts to bound them). */
|
|
90
|
+
newLeft: number;
|
|
91
|
+
newNo: number;
|
|
92
|
+
oldLeft: number;
|
|
93
|
+
oldNo: number;
|
|
94
|
+
/** True once the current file consumed a `---`/`+++` header line — the
|
|
95
|
+
* paths a `diff --git` line sets don't count (a `---` after it belongs to
|
|
96
|
+
* the same file, not a new one). */
|
|
97
|
+
seenHeader: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The active hunk still has rows to consume per its `@@` counts. */
|
|
101
|
+
const hunkLive = (st: ParseState): boolean =>
|
|
102
|
+
st.hunk !== undefined &&
|
|
103
|
+
st.hunk.header !== undefined &&
|
|
104
|
+
(st.oldLeft > 0 || st.newLeft > 0);
|
|
105
|
+
|
|
106
|
+
/** Current file already carries content → a new `---` starts a new file. */
|
|
107
|
+
const hasContent = (st: ParseState): boolean =>
|
|
108
|
+
st.cur !== undefined && (st.seenHeader || st.cur.hunks.length > 0);
|
|
109
|
+
|
|
110
|
+
const ensureFile = (st: ParseState): FileDiff => {
|
|
111
|
+
if (st.cur === undefined) {
|
|
112
|
+
st.cur = newFile();
|
|
113
|
+
st.files.push(st.cur);
|
|
114
|
+
}
|
|
115
|
+
return st.cur;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const push = (st: ParseState, line: string): FileDiff => {
|
|
119
|
+
const f = ensureFile(st);
|
|
120
|
+
f.raw.push(line);
|
|
121
|
+
return f;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** `---`/`+++` file headers. Returns true when the line was consumed. */
|
|
125
|
+
const fileHeader = (st: ParseState, line: string): boolean => {
|
|
126
|
+
const oldM = OLD_RE.exec(line);
|
|
127
|
+
if (oldM?.groups !== undefined) {
|
|
128
|
+
if (hasContent(st)) {
|
|
129
|
+
st.cur = newFile();
|
|
130
|
+
st.files.push(st.cur);
|
|
131
|
+
}
|
|
132
|
+
push(st, line).oldPath = cleanPath(oldM.groups.q ?? oldM.groups.p);
|
|
133
|
+
st.hunk = undefined;
|
|
134
|
+
st.seenHeader = true;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const newM = NEW_RE.exec(line);
|
|
138
|
+
if (newM?.groups !== undefined) {
|
|
139
|
+
push(st, line).newPath = cleanPath(newM.groups.q ?? newM.groups.p);
|
|
140
|
+
st.hunk = undefined;
|
|
141
|
+
st.seenHeader = true;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** One row inside an active hunk; tracks both line counters. */
|
|
148
|
+
const hunkRow = (st: ParseState, line: string): void => {
|
|
149
|
+
const { hunk } = st;
|
|
150
|
+
if (hunk === undefined) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Line numbers come from the `@@` tallies — an implicit hunk (bare +/-
|
|
154
|
+
// stream) has none, so its rows must not inherit stale counters from the
|
|
155
|
+
// previous counted hunk.
|
|
156
|
+
const numbered = hunk.header !== undefined;
|
|
157
|
+
const f = push(st, line);
|
|
158
|
+
if (line.startsWith("\\")) {
|
|
159
|
+
hunk.rows.push({ kind: "note", text: line });
|
|
160
|
+
} else if (line.startsWith("+")) {
|
|
161
|
+
f.adds += 1;
|
|
162
|
+
st.newLeft -= 1;
|
|
163
|
+
hunk.rows.push({
|
|
164
|
+
kind: "add",
|
|
165
|
+
newLine: numbered ? st.newNo : undefined,
|
|
166
|
+
text: line.slice(1),
|
|
167
|
+
});
|
|
168
|
+
st.newNo += 1;
|
|
169
|
+
} else if (line.startsWith("-")) {
|
|
170
|
+
f.dels += 1;
|
|
171
|
+
st.oldLeft -= 1;
|
|
172
|
+
hunk.rows.push({
|
|
173
|
+
kind: "del",
|
|
174
|
+
oldLine: numbered ? st.oldNo : undefined,
|
|
175
|
+
text: line.slice(1),
|
|
176
|
+
});
|
|
177
|
+
st.oldNo += 1;
|
|
178
|
+
} else {
|
|
179
|
+
st.oldLeft -= 1;
|
|
180
|
+
st.newLeft -= 1;
|
|
181
|
+
hunk.rows.push({
|
|
182
|
+
kind: "ctx",
|
|
183
|
+
newLine: numbered ? st.newNo : undefined,
|
|
184
|
+
oldLine: numbered ? st.oldNo : undefined,
|
|
185
|
+
text: line.startsWith(" ") ? line.slice(1) : line,
|
|
186
|
+
});
|
|
187
|
+
st.oldNo += 1;
|
|
188
|
+
st.newNo += 1;
|
|
189
|
+
}
|
|
190
|
+
// A counted hunk is done when its `@@` tallies run out — the next `---` /
|
|
191
|
+
// `diff --git` line is a file boundary again, not content.
|
|
192
|
+
if (hunk.header !== undefined && st.oldLeft <= 0 && st.newLeft <= 0) {
|
|
193
|
+
st.hunk = undefined;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A line with no hunk context: file meta (index/mode/rename/binary) or the
|
|
199
|
+
* first line of a bare +/- stream, which opens an implicit hunk.
|
|
200
|
+
*/
|
|
201
|
+
const looseLine = (st: ParseState, line: string): void => {
|
|
202
|
+
const f = push(st, line);
|
|
203
|
+
const rf = RENAME_FROM_RE.exec(line);
|
|
204
|
+
if (rf?.groups !== undefined) {
|
|
205
|
+
f.oldPath ??= cleanPath(rf.groups.p.trim());
|
|
206
|
+
}
|
|
207
|
+
const rt = RENAME_TO_RE.exec(line);
|
|
208
|
+
if (rt?.groups !== undefined) {
|
|
209
|
+
f.newPath ??= cleanPath(rt.groups.p.trim());
|
|
210
|
+
}
|
|
211
|
+
if (line.startsWith("+") || line.startsWith("-")) {
|
|
212
|
+
const kind = line.startsWith("+") ? "add" : "del";
|
|
213
|
+
f[kind === "add" ? "adds" : "dels"] += 1;
|
|
214
|
+
st.oldLeft = Number.POSITIVE_INFINITY;
|
|
215
|
+
st.newLeft = Number.POSITIVE_INFINITY;
|
|
216
|
+
st.hunk = { rows: [{ kind, text: line.slice(1) }] };
|
|
217
|
+
f.hunks.push(st.hunk);
|
|
218
|
+
} else if (line !== "") {
|
|
219
|
+
f.meta.push(line);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** One input line of `parseDiff`: git/file headers, `@@` hunk headers, and
|
|
224
|
+
* hunk/loose content each consume the line and update `st`. */
|
|
225
|
+
const parseLine = (st: ParseState, line: string, nextLine: string): void => {
|
|
226
|
+
const git = GIT_RE.exec(line);
|
|
227
|
+
if (git?.groups !== undefined) {
|
|
228
|
+
st.cur = newFile();
|
|
229
|
+
st.files.push(st.cur);
|
|
230
|
+
st.cur.raw.push(line);
|
|
231
|
+
st.cur.oldPath = cleanPath(git.groups.aq ?? git.groups.a);
|
|
232
|
+
st.cur.newPath = cleanPath(git.groups.bq ?? git.groups.b);
|
|
233
|
+
st.hunk = undefined;
|
|
234
|
+
st.seenHeader = false;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A `---`/`+++` pair is a file header — but only once the current counted
|
|
239
|
+
// hunk is exhausted. Inside a `@@` hunk body the pair is content: deleting
|
|
240
|
+
// a line like `-- a/x` and adding `++ b/x` emits exactly those rows (e.g.
|
|
241
|
+
// YAML frontmatter or nested diffs).
|
|
242
|
+
const pair = OLD_RE.test(line) && NEW_RE.test(nextLine);
|
|
243
|
+
if (
|
|
244
|
+
(st.hunk === undefined || (pair && !hunkLive(st))) &&
|
|
245
|
+
fileHeader(st, line)
|
|
246
|
+
) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const hunkM = HUNK_RE.exec(line);
|
|
251
|
+
if (hunkM?.groups !== undefined) {
|
|
252
|
+
st.hunk = { header: line, rows: [] };
|
|
253
|
+
push(st, line).hunks.push(st.hunk);
|
|
254
|
+
st.oldNo = Number(hunkM.groups.o);
|
|
255
|
+
st.newNo = Number(hunkM.groups.n);
|
|
256
|
+
st.oldLeft = Number(hunkM.groups.oc ?? "1");
|
|
257
|
+
st.newLeft = Number(hunkM.groups.nc ?? "1");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (st.hunk === undefined) {
|
|
262
|
+
// `` follows the row it annotates — the hunk's
|
|
263
|
+
// counters may already have closed it, but the note still belongs inside.
|
|
264
|
+
const last = st.cur?.hunks.at(-1);
|
|
265
|
+
if (line.startsWith("\\") && last !== undefined) {
|
|
266
|
+
push(st, line);
|
|
267
|
+
last.rows.push({ kind: "note", text: line });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
looseLine(st, line);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
hunkRow(st, line);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Parse unified-diff text into per-file structures. `--- `/`+++ ` count as
|
|
278
|
+
* file headers only outside hunks (or when they form a `---`/`+++` pair —
|
|
279
|
+
* inside a hunk a lone `-`-prefixed line is content). Lines with no diff
|
|
280
|
+
* structure at all land in `meta`/`raw`; bare `+`/`-` streams without `@@`
|
|
281
|
+
* headers collect into an implicit hunk.
|
|
282
|
+
*/
|
|
283
|
+
export const parseDiff = (text: string): FileDiff[] => {
|
|
284
|
+
// Normalize CRLF — a trailing \r would leak into row text and break the
|
|
285
|
+
// regexes that test the *next* line's structure.
|
|
286
|
+
const lines = text.replaceAll("\r\n", "\n").replace(/\n+$/u, "").split("\n");
|
|
287
|
+
const st: ParseState = {
|
|
288
|
+
files: [],
|
|
289
|
+
newLeft: 0,
|
|
290
|
+
newNo: 0,
|
|
291
|
+
oldLeft: 0,
|
|
292
|
+
oldNo: 0,
|
|
293
|
+
seenHeader: false,
|
|
294
|
+
};
|
|
295
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
296
|
+
parseLine(st, lines[i] ?? "", lines[i + 1] ?? "");
|
|
297
|
+
}
|
|
298
|
+
// Drop shells that never got content — an empty input would otherwise
|
|
299
|
+
// yield one blank file card.
|
|
300
|
+
return st.files.filter(
|
|
301
|
+
(f) =>
|
|
302
|
+
f.hunks.length > 0 ||
|
|
303
|
+
f.meta.length > 0 ||
|
|
304
|
+
f.oldPath !== undefined ||
|
|
305
|
+
f.newPath !== undefined
|
|
306
|
+
);
|
|
307
|
+
};
|