Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions browser/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const input = document.querySelector<HTMLTextAreaElement>("#source")!;
const status = document.querySelector<HTMLElement>("#status")!;
const message = document.querySelector<HTMLElement>("#message")!;
const locate = document.querySelector<HTMLButtonElement>("#locate")!;
let errorLocation: {start: number; end: number} | undefined;
let errorLocation: {start: number; end: number; line: number; column: number} | undefined;
function resetLocation() {
errorLocation = undefined;
locate.hidden = true;
Expand Down Expand Up @@ -66,9 +66,7 @@ function check() {
status.textContent = "无法解析文档";
errorLocation = parsed.location;
if (errorLocation) {
const lines = input.value.slice(0, errorLocation.start).split("\n");
const column = Array.from(lines[lines.length-1] ?? "").length + 1;
message.textContent = "第 " + lines.length + " 行,第 " + column + " 列。" + parsed.error;
message.textContent = "第 " + errorLocation.line + " 行,第 " + errorLocation.column + " 列。" + parsed.error;
locate.hidden = false;
} else {
message.textContent = parsed.error;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/diff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolve } from "node:path";

import { parseSkillDocument } from "../../domain/frontmatter.js";
import { describeDocumentError, parseSkillDocument } from "../../domain/frontmatter.js";
import { normalizeRelativePath } from "../../domain/digest.js";
import { scanInventory, type ScanTarget } from "../../domain/inventory.js";
import {
Expand Down Expand Up @@ -60,7 +60,7 @@ function hydrateFrontmatter(skill: Skill, label: "before" | "after"): Skill {

const parsed = parseSkillDocument(skillMd.content.toString("utf8"));
if (!parsed.ok) {
throw new Error(`Cannot diff ${label} Skill ${skill.name}: ${parsed.error}`);
throw new Error(`Cannot diff ${label} Skill ${skill.name}: ${describeDocumentError(parsed)}`);
}

return { ...skill, frontmatter: parsed.value.frontmatter };
Expand Down
23 changes: 18 additions & 5 deletions src/domain/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,22 @@ export type ParsedSkillDocument = {
sourceRange: { start: number; end: number };
};

type DocumentLocation = { start: number; end: number; line: number; column: number };
type DocumentFailure = { ok: false; error: string; location?: DocumentLocation };

function documentLocation(text: string, start: number, end = start): DocumentLocation {
const lines = text.slice(0, start).split("\n");
return {start, end, line: lines.length, column: Array.from(lines[lines.length - 1] ?? "").length + 1};
}

export function describeDocumentError(failure: DocumentFailure): string {
const at = failure.location;
return at ? failure.error + " (line " + at.line + ", column " + at.column + ")" : failure.error;
}

export type ParseSkillDocumentResult =
| { ok: true; value: ParsedSkillDocument }
| { ok: false; error: string; location?: { start: number; end: number } };
| DocumentFailure;

function parserError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
Expand All @@ -20,7 +33,7 @@ export function parseSkillDocument(content: string): ParseSkillDocumentResult {
if (!opening) {
return {
ok: false,
location: { start: 0, end: 0 },
location: documentLocation(content, 0),
error: "Skill document must start with a YAML frontmatter delimiter.",
};
}
Expand All @@ -30,7 +43,7 @@ export function parseSkillDocument(content: string): ParseSkillDocumentResult {
if (!closing) {
return {
ok: false,
location: { start: content.length, end: content.length },
location: documentLocation(content, content.length),
error: "Skill frontmatter closing delimiter is missing.",
};
}
Expand All @@ -44,7 +57,7 @@ export function parseSkillDocument(content: string): ParseSkillDocumentResult {
parsed = parse(frontmatterText, { uniqueKeys: true, prettyErrors: false });
} catch (error: unknown) {
const location = error instanceof YAMLParseError
? { start: Math.min(yamlEnd, openingEnd + error.pos[0]), end: Math.min(yamlEnd, openingEnd + error.pos[1]) }
? documentLocation(content, Math.min(yamlEnd, openingEnd + error.pos[0]), Math.min(yamlEnd, openingEnd + error.pos[1]))
: undefined;
return {
...(location && { location }),
Expand All @@ -61,7 +74,7 @@ export function parseSkillDocument(content: string): ParseSkillDocumentResult {
return {
ok: false,
error: "Skill frontmatter must be a YAML mapping.",
location: { start: openingEnd, end: yamlEnd },
location: documentLocation(content, openingEnd, yamlEnd),
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/scanners/structure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { basename } from "node:path";

import { parseSkillDocument } from "../domain/frontmatter.js";
import { describeDocumentError, parseSkillDocument } from "../domain/frontmatter.js";
import { normalizeRelativePath } from "../domain/digest.js";
import type { Finding } from "../domain/result.js";
import type { Skill, SkillFile } from "../domain/skill.js";
Expand Down Expand Up @@ -160,7 +160,7 @@ export function inspectStructure(skill: Skill): Finding[] {
severity: "error",
status: "fail",
code: "structure.invalid-frontmatter",
message: parsed.error,
message: describeDocumentError(parsed),
evidence: [{ path: "SKILL.md" }],
remediation: "Add valid YAML frontmatter enclosed by --- delimiters.",
}),
Expand Down
6 changes: 5 additions & 1 deletion tests/domain/frontmatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ describe("parseSkillDocument", () => {
const text = "---\r\nname: 鱼🐟\r\nname: b\r\n---\r\nbody";
const result = parseSkillDocument(text);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.location?.start).toBe(text.lastIndexOf("name:"));
if (!result.ok) {
expect(result.location?.start).toBe(text.lastIndexOf("name:"));
expect(result.location?.line).toBe(3);
expect(result.location?.column).toBe(1);
}
});

it("rejects a document without an opening or closing delimiter", () => {
Expand Down
Loading