diff --git a/src/commands/docs-list.ts b/src/commands/docs-list.ts new file mode 100644 index 0000000..09b005b --- /dev/null +++ b/src/commands/docs-list.ts @@ -0,0 +1,89 @@ +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; + +const DOCS_INDEX_URL = new URL("https://prismic.io/docs/api/index/"); + +const config = { + name: "prismic docs list", + description: ` + List available documentation pages. + + With a path argument, list the anchors within that page. + `, + positionals: { + path: { + description: "Documentation path to list anchors for", + required: false, + }, + }, + options: { + json: { type: "boolean", description: "Output as JSON" }, + }, +} satisfies CommandConfig; + +type IndexPage = { + path: string; + title: string; + description: string; +}; + +type IndexPageWithAnchors = IndexPage & { + anchors: { slug: string; excerpt: string }[]; +}; + +export default createCommand(config, async ({ positionals, values }) => { + const [path] = positionals; + const { json } = values; + + if (path) { + const url = new URL(path, DOCS_INDEX_URL); + const response = await fetch(url); + + if (!response.ok) { + if (response.status === 404) { + throw new CommandError(`Documentation page not found: ${path}`); + } + throw new CommandError(`Failed to fetch documentation index: ${response.statusText}`); + } + + const entry: IndexPageWithAnchors = await response.json(); + entry.anchors.sort((a, b) => a.slug.localeCompare(b.slug)); + + if (json) { + console.info(stringify(entry)); + return; + } + + if (entry.anchors.length === 0) { + console.info("(no anchors)"); + return; + } + + for (const anchor of entry.anchors) { + console.info(`${path}#${anchor.slug}: ${anchor.excerpt}`); + } + } else { + const response = await fetch(DOCS_INDEX_URL); + + if (!response.ok) { + throw new CommandError(`Failed to fetch documentation index: ${response.statusText}`); + } + + const pages: IndexPage[] = await response.json(); + pages.sort((a, b) => a.path.localeCompare(b.path)); + + if (json) { + console.info(stringify(pages)); + return; + } + + if (pages.length === 0) { + console.info("No documentation pages found."); + return; + } + + for (const page of pages) { + console.info(`${page.path}: ${page.title} — ${page.description}`); + } + } +}); diff --git a/src/commands/docs-view.ts b/src/commands/docs-view.ts new file mode 100644 index 0000000..f5a5f91 --- /dev/null +++ b/src/commands/docs-view.ts @@ -0,0 +1,95 @@ +import { CommandError, createCommand, type CommandConfig } from "../lib/command"; +import { stringify } from "../lib/json"; + +const DOCS_BASE_URL = new URL("https://prismic.io/docs/"); + +const config = { + name: "prismic docs view", + description: ` + View a documentation page as Markdown. + + Append #anchor to the path to view only the section under that heading. + `, + positionals: { + path: { + description: "Documentation path, optionally with #anchor (e.g., setup#install)", + required: true, + }, + }, + options: { + json: { type: "boolean", description: "Output as JSON" }, + }, +} satisfies CommandConfig; + +export default createCommand(config, async ({ positionals, values }) => { + const [rawPath] = positionals; + const { json } = values; + + const hashIndex = rawPath.indexOf("#"); + const path = hashIndex >= 0 ? rawPath.slice(0, hashIndex) : rawPath; + const anchor = hashIndex >= 0 ? rawPath.slice(hashIndex + 1) : undefined; + + const url = new URL(path, DOCS_BASE_URL); + const response = await fetch(url, { + headers: { Accept: "text/markdown" }, + }); + + if (!response.ok) { + throw new CommandError(`Failed to fetch documentation page: ${response.statusText}`); + } + + let markdown = await response.text(); + + if (anchor) { + const section = extractSection(markdown, anchor); + if (!section) { + throw new CommandError(`Anchor not found: #${anchor}`); + } + markdown = section; + } + + if (json) { + console.info(stringify({ path, anchor, content: markdown })); + return; + } + + console.info(markdown); +}); + +function extractSection(markdown: string, anchor: string): string | undefined { + const lines = markdown.split("\n"); + let startIndex = -1; + let headingLevel = 0; + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^(#{1,6})\s+(.*)/); + if (!match) { + continue; + } + + const level = match[1].length; + const text = match[2]; + + if (startIndex >= 0 && level <= headingLevel) { + return lines.slice(startIndex, i).join("\n").trimEnd(); + } + + if (kebabCase(text) === anchor) { + startIndex = i; + headingLevel = level; + } + } + + if (startIndex >= 0) { + return lines.slice(startIndex).join("\n").trimEnd(); + } + + return undefined; +} + +function kebabCase(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); +} diff --git a/src/commands/docs.ts b/src/commands/docs.ts new file mode 100644 index 0000000..39cd4ba --- /dev/null +++ b/src/commands/docs.ts @@ -0,0 +1,19 @@ +import { createCommandRouter } from "../lib/command"; + +import docsList from "./docs-list"; +import docsView from "./docs-view"; + +export default createCommandRouter({ + name: "prismic docs", + description: "Browse Prismic documentation.", + commands: { + list: { + handler: docsList, + description: "List available documentation pages", + }, + view: { + handler: docsView, + description: "View a documentation page", + }, + }, +}); diff --git a/src/index.ts b/src/index.ts index 8f9ff5e..8e6d4ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import packageJson from "../package.json" with { type: "json" }; import { getAdapter, NoSupportedFrameworkError } from "./adapters"; import { getHost, refreshToken } from "./auth"; import { getProfile } from "./clients/user"; +import docs from "./commands/docs"; import gen from "./commands/gen"; import init from "./commands/init"; import locale from "./commands/locale"; @@ -37,7 +38,7 @@ import { import { dedent } from "./lib/string"; import { safeGetRepositoryName, TypeBuilderRequiredError } from "./project"; -const UNTRACKED_COMMANDS = ["login", "logout", "whoami", "sync"]; +const UNTRACKED_COMMANDS = ["login", "logout", "whoami", "sync", "docs"]; const SKIP_REFRESH_COMMANDS = ["login", "logout"]; const router = createCommandRouter({ @@ -48,6 +49,10 @@ const router = createCommandRouter({ handler: init, description: "Initialize a Prismic project", }, + docs: { + handler: docs, + description: "Browse Prismic documentation", + }, gen: { handler: gen, description: "Generate files from local models",