-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add docs command for browsing Prismic documentation
#85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7736a3d
92e93c9
a18c75a
d2e530d
16dbdd9
d89c484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import * as z from "zod/mini"; | ||
|
|
||
| import { DEFAULT_PRISMIC_HOST, env } from "../env"; | ||
| import { request } from "../lib/request"; | ||
|
|
||
| const DocsIndexEntrySchema = z.object({ | ||
| path: z.string(), | ||
| title: z.string(), | ||
| description: z.optional(z.string()), | ||
| }); | ||
| type DocsIndexEntry = z.infer<typeof DocsIndexEntrySchema>; | ||
|
|
||
| const DocsPageSchema = z.object({ | ||
| path: z.string(), | ||
| title: z.string(), | ||
| description: z.optional(z.string()), | ||
| anchors: z.array( | ||
| z.object({ | ||
| slug: z.string(), | ||
| excerpt: z.string(), | ||
| }), | ||
| ), | ||
| }); | ||
| type DocsPage = z.infer<typeof DocsPageSchema>; | ||
|
|
||
| export async function getDocsIndex(): Promise<DocsIndexEntry[]> { | ||
| const url = new URL("api/index/", getDocsBaseUrl()); | ||
| return await request(url, { schema: z.array(DocsIndexEntrySchema) }); | ||
| } | ||
|
|
||
| export async function getDocsPageIndex(path: string): Promise<DocsPage> { | ||
| const url = new URL(`api/index/${path}`, getDocsBaseUrl()); | ||
| return await request(url, { schema: DocsPageSchema }); | ||
| } | ||
|
|
||
| export async function getDocsPageContent(path: string): Promise<string> { | ||
| const url = new URL(path, getDocsBaseUrl()); | ||
| return await request(url, { | ||
| headers: { Accept: "text/markdown" }, | ||
| schema: z.string(), | ||
| }); | ||
| } | ||
|
|
||
| function getDocsBaseUrl(): URL { | ||
| const host = env.PRISMIC_DOCS_HOST ?? DEFAULT_PRISMIC_HOST; | ||
| return new URL(`https://${host}/docs/`); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { getDocsIndex, getDocsPageIndex } from "../clients/docs"; | ||
| import { CommandError, createCommand, type CommandConfig } from "../lib/command"; | ||
| import { stringify } from "../lib/json"; | ||
| import { NotFoundRequestError, UnknownRequestError } from "../lib/request"; | ||
|
|
||
| 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; | ||
|
|
||
| export default createCommand(config, async ({ positionals, values }) => { | ||
| const [path] = positionals; | ||
| const { json } = values; | ||
|
|
||
| if (path) { | ||
| let entry; | ||
| try { | ||
| entry = await getDocsPageIndex(path); | ||
| } catch (error) { | ||
| if (error instanceof NotFoundRequestError) { | ||
| throw new CommandError(`Documentation page not found: ${path}`); | ||
| } | ||
| if (error instanceof UnknownRequestError) { | ||
| const message = await error.text(); | ||
| throw new CommandError(`Failed to fetch documentation index: ${message}`); | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| 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 { | ||
| let pages; | ||
| try { | ||
| pages = await getDocsIndex(); | ||
| } catch (error) { | ||
| if (error instanceof UnknownRequestError) { | ||
| const message = await error.text(); | ||
| throw new CommandError(`Failed to fetch documentation index: ${message}`); | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| 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) { | ||
| const description = page.description ? ` — ${page.description}` : ""; | ||
| console.info(`${page.path}: ${page.title}${description}`); | ||
| } | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import GithubSlugger from "github-slugger"; | ||
|
|
||
| import { getDocsPageContent } from "../clients/docs"; | ||
| import { CommandError, createCommand, type CommandConfig } from "../lib/command"; | ||
| import { stringify } from "../lib/json"; | ||
| import { NotFoundRequestError, UnknownRequestError } from "../lib/request"; | ||
|
|
||
| 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; | ||
|
|
||
| let markdown: string; | ||
| try { | ||
| markdown = await getDocsPageContent(path); | ||
| } catch (error) { | ||
| if (error instanceof NotFoundRequestError) { | ||
| throw new CommandError(`Page not found: ${path}`); | ||
| } | ||
| if (error instanceof UnknownRequestError) { | ||
| const message = await error.text(); | ||
| throw new CommandError(`Failed to fetch page: ${message}`); | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| 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"); | ||
| const slugger = new GithubSlugger(); | ||
|
|
||
| let currentFence: string | undefined; | ||
| let startIndex: number | undefined; | ||
| let endIndex: number | undefined; | ||
| let headingLevel: number | undefined; | ||
|
|
||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
|
|
||
| const fenceMatch = line.match(/^(?<fence>`{3,}|~{3,})/); | ||
| const fence = fenceMatch?.groups?.fence; | ||
| if (currentFence) { | ||
| if (fence?.startsWith(currentFence)) currentFence = undefined; | ||
| continue; | ||
| } else if (fence) { | ||
| currentFence = fence; | ||
| continue; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fence closing check ignores trailing content on lineMedium Severity The fence-closing check on line 79 only verifies the line starts with the same fence characters via Reviewed by Cursor Bugbot for commit d89c484. Configure here. |
||
|
|
||
| const headingMatch = line.match(/^(?<level>#{1,6})\s+(?<text>.*)/); | ||
| if (headingMatch?.groups?.level && headingMatch?.groups?.text) { | ||
| if (startIndex !== undefined && headingLevel !== undefined) { | ||
| if (headingMatch.groups.level.length <= headingLevel) { | ||
| endIndex = i; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| const headingAnchor = slugger.slug(headingMatch.groups.text); | ||
| if (headingAnchor === anchor) { | ||
| startIndex = i; | ||
| headingLevel = headingMatch.groups.level.length; | ||
| } | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (startIndex === undefined) return; | ||
|
|
||
| return lines.slice(startIndex, endIndex).join("\n").trim(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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", | ||
| }, | ||
| }, | ||
| }); |


Uh oh!
There was an error while loading. Please reload this page.