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
16 changes: 7 additions & 9 deletions packages/cli/src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ interface ConvertCommandOptions {
}

/** Build the shared convert options from CLI flags. */
function buildConvertOptions(inputPath: string, outputPath: string, options: ConvertCommandOptions) {
function buildConvertOptions(
inputPath: string,
outputPath: string,
options: ConvertCommandOptions,
) {
const hasSelectiveFlags =
options.includeEditorialNotes || options.includeStatutoryNotes || options.includeAmendments;
const includeNotes = hasSelectiveFlags ? false : options.includeNotes;
Expand Down Expand Up @@ -159,11 +163,7 @@ export const convertCommand = new Command("convert")
.argument("[input]", "Path to a USC XML file")
.option("-o, --output <dir>", "Output directory", "./output")
.option("--titles <spec>", "Title(s) to convert (e.g. 1, 1-5, 1,3,8, 1-5,8,11)")
.option(
"-i, --input-dir <dir>",
"Directory containing USC XML files",
"./downloads/usc/xml",
)
.option("-i, --input-dir <dir>", "Directory containing USC XML files", "./downloads/usc/xml")
.option(
"-g, --granularity <level>",
'Output granularity: "section" (one file per section) or "chapter" (sections inline)',
Expand All @@ -186,9 +186,7 @@ export const convertCommand = new Command("convert")
.action(async (input: string | undefined, options: ConvertCommandOptions) => {
// Validate: must specify <input> or --titles
if (!input && !options.titles) {
console.error(
error("Specify an input file or --titles <spec> (e.g. --titles 1-5,8,11)"),
);
console.error(error("Specify an input file or --titles <spec> (e.g. --titles 1-5,8,11)"));
process.exit(1);
}

Expand Down
7 changes: 2 additions & 5 deletions packages/cli/src/commands/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ export const downloadCommand = new Command("download")
const outputDir = resolve(options.output);
const titleCount = titles ? titles.length : 54;
const label =
titleCount === 1
? `Downloading Title ${titles?.[0]}`
: `Downloading ${titleCount} titles`;
titleCount === 1 ? `Downloading Title ${titles?.[0]}` : `Downloading ${titleCount} titles`;

const spinner = createSpinner(`${label}...`);
spinner.start();
Expand Down Expand Up @@ -110,8 +108,7 @@ export const downloadCommand = new Command("download")
// Footer
const titleWord = result.files.length === 1 ? "title" : "titles";
const summary = `Downloaded ${result.files.length} ${titleWord} (${formatBytes(totalBytes)}) in ${formatDuration(elapsed)}`;
const failSuffix =
result.errors.length > 0 ? ` (${result.errors.length} failed)` : "";
const failSuffix = result.errors.length > 0 ? ` (${result.errors.length} failed)` : "";
console.log(` ${success(summary + failSuffix)}`);
console.log("");
} catch (err) {
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ function visualLength(s: string): number {
* Compute column widths that fill the terminal.
* Expands `flexCol` to absorb remaining space.
*/
function fillWidths(
columns: string[][],
colCount: number,
flexCol: number,
): number[] | undefined {
function fillWidths(columns: string[][], colCount: number, flexCol: number): number[] | undefined {
const termWidth = process.stdout.columns || 80;
// Compute natural width per column (max visual length across all rows)
const natural = Array.from<number>({ length: colCount }).fill(0);
Expand Down
21 changes: 16 additions & 5 deletions packages/core/src/ast/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ function parseAndCollect(xml: string, emitAt: ASTBuilderOptions["emitAt"] = "sec
}

/** Helper: parse a fixture file and collect emitted sections */
async function parseFileAndCollect(filename: string, emitAt: ASTBuilderOptions["emitAt"] = "section") {
async function parseFileAndCollect(
filename: string,
emitAt: ASTBuilderOptions["emitAt"] = "section",
) {
const emitted: Array<{ node: LevelNode; context: EmitContext }> = [];

const builder = new ASTBuilder({
Expand Down Expand Up @@ -103,14 +106,17 @@ describe("ASTBuilder", () => {
const section = emitted[0]!.node;

// Find the content node
const contentNode = section.children.find((c) => c.type === "content") as ContentNode | undefined;
const contentNode = section.children.find((c) => c.type === "content") as
| ContentNode
| undefined;
expect(contentNode).toBeDefined();
expect(contentNode!.variant).toBe("content");
expect(contentNode!.children.length).toBeGreaterThan(0);

// Check that there is text
const hasText = contentNode!.children.some(
(c) => c.type === "inline" && c.inlineType === "text" && c.text && c.text.includes("county"),
(c) =>
c.type === "inline" && c.inlineType === "text" && c.text && c.text.includes("county"),
);
expect(hasText).toBe(true);
});
Expand All @@ -119,7 +125,9 @@ describe("ASTBuilder", () => {
const { emitted } = await parseFileAndCollect("simple-section.xml");
const section = emitted[0]!.node;

const sourceCredit = section.children.find((c) => c.type === "sourceCredit") as SourceCreditNode | undefined;
const sourceCredit = section.children.find((c) => c.type === "sourceCredit") as
| SourceCreditNode
| undefined;
expect(sourceCredit).toBeDefined();
expect(sourceCredit!.children.length).toBeGreaterThan(0);
});
Expand Down Expand Up @@ -175,7 +183,10 @@ describe("ASTBuilder", () => {
parser.on("closeElement", (name) => builder.onCloseElement(name));
parser.on("text", (text) => builder.onText(text));

const stream = createReadStream(resolve(FIXTURES_DIR, "section-with-subsections.xml"), "utf-8");
const stream = createReadStream(
resolve(FIXTURES_DIR, "section-with-subsections.xml"),
"utf-8",
);
await parser.parseStream(stream);

expect(onEmit).toHaveBeenCalledTimes(1);
Expand Down
86 changes: 72 additions & 14 deletions packages/core/src/ast/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,16 @@ const INLINE_TYPE_MAP: Readonly<Record<string, InlineType>> = {
*/
interface StackFrame {
/** What kind of frame this is */
kind: "level" | "content" | "inline" | "note" | "sourceCredit" | "notesContainer" | "quotedContent" | "meta" | "ignore";
kind:
| "level"
| "content"
| "inline"
| "note"
| "sourceCredit"
| "notesContainer"
| "quotedContent"
| "meta"
| "ignore";
/** The AST node being constructed (null for meta/ignore frames) */
node: ASTNode | null;
/** The XML element name that opened this frame */
Expand Down Expand Up @@ -151,8 +160,14 @@ export class ASTBuilder {

if (name === "xhtml:table") {
this.tableCollector = {
headers: [], rows: [], currentRow: [], cellText: "",
inHead: false, inCell: false, isComplex: false, cellDepth: 0,
headers: [],
rows: [],
currentRow: [],
cellText: "",
inHead: false,
inCell: false,
isComplex: false,
cellDepth: 0,
};
return;
}
Expand All @@ -163,8 +178,14 @@ export class ASTBuilder {

if (name === "layout") {
this.layoutCollector = {
headers: [], rows: [], currentRow: [], cellText: "",
inHead: false, inCell: false, isComplex: false, cellDepth: 0,
headers: [],
rows: [],
currentRow: [],
cellText: "",
inHead: false,
inCell: false,
isComplex: false,
cellDepth: 0,
};
return;
}
Expand Down Expand Up @@ -207,7 +228,12 @@ export class ASTBuilder {
return;
}

if (name === "note" || name === "statutoryNote" || name === "editorialNote" || name === "changeNote") {
if (
name === "note" ||
name === "statutoryNote" ||
name === "editorialNote" ||
name === "changeNote"
) {
this.openNote(name, attrs);
return;
}
Expand Down Expand Up @@ -349,7 +375,12 @@ export class ASTBuilder {
}

// Handle note close
if (name === "note" || name === "statutoryNote" || name === "editorialNote" || name === "changeNote") {
if (
name === "note" ||
name === "statutoryNote" ||
name === "editorialNote" ||
name === "changeNote"
) {
this.closeNote();
return;
}
Expand Down Expand Up @@ -455,7 +486,11 @@ export class ASTBuilder {
const trimmed = text.trim();
if (trimmed) {
const textNode: InlineNode = { type: "inline", inlineType: "text", text };
const contentNode: ContentNode = { type: "content", variant: "content", children: [textNode] };
const contentNode: ContentNode = {
type: "content",
variant: "content",
children: [textNode],
};
const parent = frame.node;
if (parent && "children" in parent && Array.isArray(parent.children)) {
(parent.children as ASTNode[]).push(contentNode);
Expand All @@ -469,7 +504,11 @@ export class ASTBuilder {
const trimmed = text.trim();
if (trimmed) {
const textNode: InlineNode = { type: "inline", inlineType: "text", text };
const contentNode: ContentNode = { type: "content", variant: "content", children: [textNode] };
const contentNode: ContentNode = {
type: "content",
variant: "content",
children: [textNode],
};
(frame.node as LevelNode).children.push(contentNode);
}
return;
Expand Down Expand Up @@ -741,7 +780,12 @@ export class ASTBuilder {
// Add as inline "quoted" node if parent is content/inline,
// or as block node if parent is note/level
const parentFrame = this.peekFrame();
if (parentFrame && (parentFrame.kind === "content" || parentFrame.kind === "inline" || parentFrame.kind === "sourceCredit")) {
if (
parentFrame &&
(parentFrame.kind === "content" ||
parentFrame.kind === "inline" ||
parentFrame.kind === "sourceCredit")
) {
// Flatten to inline quoted text
const qNode: InlineNode = {
type: "inline",
Expand All @@ -766,7 +810,11 @@ export class ASTBuilder {
const levelFrame = this.findParentFrame("level");

// Heading inside a note takes priority
if (name === "heading" && noteFrame && (!levelFrame || this.stack.indexOf(noteFrame) > this.stack.indexOf(levelFrame))) {
if (
name === "heading" &&
noteFrame &&
(!levelFrame || this.stack.indexOf(noteFrame) > this.stack.indexOf(levelFrame))
) {
(noteFrame.node as NoteNode).heading = text;
return;
}
Expand All @@ -783,7 +831,9 @@ export class ASTBuilder {
}

// Update ancestor entry if this is a big level
const ancestor = this.ancestors.find((a) => a.levelType === levelNode.levelType && a.identifier === levelNode.identifier);
const ancestor = this.ancestors.find(
(a) => a.levelType === levelNode.levelType && a.identifier === levelNode.identifier,
);
if (ancestor) {
if (name === "num") {
ancestor.numValue = levelNode.numValue;
Expand Down Expand Up @@ -812,10 +862,18 @@ export class ASTBuilder {
}
children.push(textNode);
}
} else if (parentFrame.kind === "note" || parentFrame.kind === "level" || parentFrame.kind === "quotedContent") {
} else if (
parentFrame.kind === "note" ||
parentFrame.kind === "level" ||
parentFrame.kind === "quotedContent"
) {
// Wrap in a ContentNode
const textNode: InlineNode = { type: "inline", inlineType: "text", text };
const contentNode: ContentNode = { type: "content", variant: "content", children: [textNode] };
const contentNode: ContentNode = {
type: "content",
variant: "content",
children: [textNode],
};
const parent = parentFrame.node;
if (parent && "children" in parent && Array.isArray(parent.children)) {
(parent.children as ASTNode[]).push(contentNode);
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/ast/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,18 @@ describe("AST types", () => {

it("creates valid AncestorInfo", () => {
const ancestors: AncestorInfo[] = [
{ levelType: "title", numValue: "1", heading: "GENERAL PROVISIONS", identifier: "/us/usc/t1" },
{ levelType: "chapter", numValue: "1", heading: "RULES OF CONSTRUCTION", identifier: "/us/usc/t1/ch1" },
{
levelType: "title",
numValue: "1",
heading: "GENERAL PROVISIONS",
identifier: "/us/usc/t1",
},
{
levelType: "chapter",
numValue: "1",
heading: "RULES OF CONSTRUCTION",
identifier: "/us/usc/t1/ch1",
},
];
expect(ancestors).toHaveLength(2);
expect(ancestors[0]!.levelType).toBe("title");
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,17 @@ export interface ContentNode extends BaseNode {
}

/** Discriminator for inline node types */
export type InlineType = "text" | "bold" | "italic" | "ref" | "date" | "term" | "quoted" | "sup" | "sub" | "footnoteRef";
export type InlineType =
| "text"
| "bold"
| "italic"
| "ref"
| "date"
| "term"
| "quoted"
| "sup"
| "sub"
| "footnoteRef";

/** Inline text or formatting */
export interface InlineNode extends BaseNode {
Expand Down
7 changes: 1 addition & 6 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ export {
} from "./xml/namespace.js";

// AST types
export {
LEVEL_TYPES,
BIG_LEVELS,
SMALL_LEVELS,
} from "./ast/types.js";
export { LEVEL_TYPES, BIG_LEVELS, SMALL_LEVELS } from "./ast/types.js";
export type {
LevelType,
LevelNode,
Expand Down Expand Up @@ -56,4 +52,3 @@ export type { RenderOptions, NotesFilter } from "./markdown/renderer.js";
export { generateFrontmatter, FORMAT_VERSION, GENERATOR } from "./markdown/frontmatter.js";
export { createLinkResolver, parseIdentifier } from "./markdown/links.js";
export type { LinkResolver, ParsedIdentifier } from "./markdown/links.js";

2 changes: 1 addition & 1 deletion packages/core/src/markdown/frontmatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FrontmatterData } from "../ast/types.js";
/** Minimal valid frontmatter data */
const MINIMAL_DATA: FrontmatterData = {
identifier: "/us/usc/t1/s1",
title: '1 USC § 1 - Words denoting number, gender, and so forth',
title: "1 USC § 1 - Words denoting number, gender, and so forth",
title_number: 1,
title_name: "General Provisions",
section_number: "1",
Expand Down
Loading
Loading