Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
28 changes: 26 additions & 2 deletions docs.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,19 @@
}
},
"toc": {
"type": "boolean"
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["tree", "compressed"]
}
]
},
"tocFormat": {
"type": "string",
"enum": ["tree", "compressed"]
}
},
"additionalProperties": false
Expand Down Expand Up @@ -146,7 +158,19 @@
"additionalProperties": false
},
"toc": {
"type": "boolean"
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": ["tree", "compressed"]
}
]
},
"tocFormat": {
"type": "string",
"enum": ["tree", "compressed"]
}
},
"required": ["id", "repo"],
Expand Down
7 changes: 5 additions & 2 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

export const TargetModeSchema = z.enum(["symlink", "copy"]);
export const CacheModeSchema = z.enum(["materialize"]);
export const TocFormatSchema = z.enum(["tree", "compressed"]);
export const IntegritySchema = z
.object({
type: z.enum(["commit", "manifest"]),
Expand All @@ -20,7 +21,8 @@ export const DefaultsSchema = z
maxBytes: z.number().min(1),
maxFiles: z.number().min(1).optional(),
allowHosts: z.array(z.string().min(1)).min(1),
toc: z.boolean().optional(),
toc: z.union([z.boolean(), TocFormatSchema]).optional(),
tocFormat: TocFormatSchema.optional(),
})
.strict();

Expand All @@ -39,7 +41,8 @@ export const SourceSchema = z
maxBytes: z.number().min(1).optional(),
maxFiles: z.number().min(1).optional(),
integrity: IntegritySchema.optional(),
toc: z.boolean().optional(),
toc: z.union([z.boolean(), TocFormatSchema]).optional(),
tocFormat: TocFormatSchema.optional(),
})
.strict();

Expand Down
67 changes: 57 additions & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { assertSafeSourceId } from "./source-id";

export type CacheMode = "materialize";

export type TocFormat = "tree" | "compressed";

export type IntegrityType = "commit" | "manifest";

export interface DocsCacheIntegrity {
Expand All @@ -23,7 +25,8 @@ export interface DocsCacheDefaults {
maxBytes: number;
maxFiles?: number;
allowHosts: string[];
toc?: boolean;
toc?: boolean | TocFormat;
tocFormat?: TocFormat;
}

export interface DocsCacheSource {
Expand All @@ -40,7 +43,8 @@ export interface DocsCacheSource {
maxBytes?: number;
maxFiles?: number;
integrity?: DocsCacheIntegrity;
toc?: boolean;
toc?: boolean | TocFormat;
tocFormat?: TocFormat;
}

export interface DocsCacheConfig {
Expand All @@ -65,7 +69,8 @@ export interface DocsCacheResolvedSource {
maxBytes: number;
maxFiles?: number;
integrity?: DocsCacheIntegrity;
toc?: boolean;
toc?: boolean | TocFormat;
tocFormat?: TocFormat;
}

export const DEFAULT_CONFIG_FILENAME = "docs.config.json";
Expand All @@ -83,7 +88,7 @@ export const DEFAULT_CONFIG: DocsCacheConfig = {
required: true,
maxBytes: 200000000,
allowHosts: ["github.com", "gitlab.com"],
toc: true,
tocFormat: "compressed",
},
sources: [],
};
Expand Down Expand Up @@ -233,6 +238,29 @@ const assertIntegrity = (value: unknown, label: string): DocsCacheIntegrity => {
return { type, value: integrityValue };
};

const normalizeTocConfig = (
toc: boolean | TocFormat | undefined,
tocFormat: TocFormat | undefined,
): { toc?: boolean | TocFormat; tocFormat?: TocFormat } => {
// If tocFormat is explicitly set, use it (and keep toc if it was set too)
if (tocFormat !== undefined) {
if (toc !== undefined) {
return { toc, tocFormat };
}
return { tocFormat };
}
// If toc is a format string, set tocFormat but also keep toc
if (typeof toc === "string") {
return { toc, tocFormat: toc };
}
// If toc is a boolean, keep it and set tocFormat accordingly
if (typeof toc === "boolean") {
return { toc, tocFormat: toc ? "compressed" : undefined };
Comment thread
fbosch marked this conversation as resolved.
Outdated
}
// Default case - no toc or tocFormat provided
return {};
};

export const validateConfig = (input: unknown): DocsCacheConfig => {
if (!isRecord(input)) {
throw new Error("Config must be a JSON object.");
Expand Down Expand Up @@ -260,6 +288,13 @@ export const validateConfig = (input: unknown): DocsCacheConfig => {
if (!isRecord(defaultsInput)) {
throw new Error("defaults must be an object.");
}

// Normalize toc/tocFormat config for defaults
const normalizedToc = normalizeTocConfig(
defaultsInput.toc as boolean | TocFormat | undefined,
defaultsInput.tocFormat as TocFormat | undefined,
);

defaults = {
ref:
defaultsInput.ref !== undefined
Expand Down Expand Up @@ -297,10 +332,11 @@ export const validateConfig = (input: unknown): DocsCacheConfig => {
defaultsInput.allowHosts !== undefined
? assertStringArray(defaultsInput.allowHosts, "defaults.allowHosts")
: defaultValues.allowHosts,
toc:
defaultsInput.toc !== undefined
? assertBoolean(defaultsInput.toc, "defaults.toc")
: defaultValues.toc,
toc: normalizedToc.toc,
tocFormat:
normalizedToc.tocFormat !== undefined
? normalizedToc.tocFormat
: defaultValues.tocFormat,
};
} else if (targetModeOverride !== undefined) {
defaults = {
Expand Down Expand Up @@ -387,9 +423,19 @@ export const validateConfig = (input: unknown): DocsCacheConfig => {
`sources[${index}].integrity`,
);
}
if (entry.toc !== undefined) {
source.toc = assertBoolean(entry.toc, `sources[${index}].toc`);

// Normalize toc/tocFormat config for this source
const normalizedToc = normalizeTocConfig(
entry.toc as boolean | TocFormat | undefined,
entry.tocFormat as TocFormat | undefined,
);
if (normalizedToc.toc !== undefined) {
source.toc = normalizedToc.toc;
}
if (normalizedToc.tocFormat !== undefined) {
source.tocFormat = normalizedToc.tocFormat;
}

return source;
});

Expand Down Expand Up @@ -436,6 +482,7 @@ export const resolveSources = (
maxFiles: source.maxFiles ?? defaults.maxFiles,
integrity: source.integrity,
toc: source.toc ?? defaults.toc,
tocFormat: source.tocFormat ?? defaults.tocFormat,
}));
};

Expand Down
50 changes: 42 additions & 8 deletions src/toc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { access, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import type { DocsCacheResolvedSource } from "./config";
import type { DocsCacheResolvedSource, TocFormat } from "./config";
import type { DocsCacheLock } from "./lock";
import { DEFAULT_TOC_FILENAME, resolveTargetDir, toPosixPath } from "./paths";

Expand Down Expand Up @@ -69,7 +69,20 @@ const renderTocTree = (tree: TocTree, depth: number, lines: string[]) => {
}
};

const generateSourceToc = (entry: TocEntry): string => {
const renderCompressedToc = (files: string[], lines: string[]) => {
// Sort files alphabetically
const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));

// Render as a flat list with paths
for (const file of sortedFiles) {
lines.push(`- [${file}](./${file})`);
}
};

const generateSourceToc = (
entry: TocEntry,
format: TocFormat = "compressed",
): string => {
const lines: string[] = [];
lines.push("---");
lines.push(`id: ${entry.id}`);
Expand All @@ -85,8 +98,15 @@ const generateSourceToc = (entry: TocEntry): string => {
lines.push("");
lines.push("## Files");
lines.push("");
const tree = createTocTree(entry.files);
renderTocTree(tree, 0, lines);

if (format === "tree") {
const tree = createTocTree(entry.files);
renderTocTree(tree, 0, lines);
} else {
// compressed format
renderCompressedToc(entry.files, lines);
}

lines.push("");

return lines.join("\n");
Expand Down Expand Up @@ -154,11 +174,25 @@ export const writeToc = async (params: {
files,
};

// Generate per-source TOC if the source has TOC enabled
const sourceToc = source?.toc ?? true;
// Determine if TOC should be generated and what format to use
const sourceTocConfig = source?.toc;
const sourceTocFormat = source?.tocFormat;

// Determine if TOC is enabled (default: true)
const tocEnabled = sourceTocConfig !== false;

// Determine TOC format
let tocFormat: TocFormat = "compressed"; // default
if (sourceTocFormat) {
tocFormat = sourceTocFormat;
} else if (typeof sourceTocConfig === "string") {
// Backward compatibility: if toc is a format string, use it
tocFormat = sourceTocConfig;
}

const sourceTocPath = path.join(sourceDir, DEFAULT_TOC_FILENAME);

if (sourceToc) {
if (tocEnabled) {
const result = resultsById.get(id);
if (result?.status === "up-to-date") {
try {
Expand All @@ -168,7 +202,7 @@ export const writeToc = async (params: {
// Missing TOC; regenerate below.
}
}
const sourceTocContent = generateSourceToc(entry);
const sourceTocContent = generateSourceToc(entry, tocFormat);
await writeFile(sourceTocPath, sourceTocContent, "utf8");
} else {
// Remove TOC.md if it exists but toc is disabled
Expand Down
Loading
Loading