Skip to content
Draft
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
89 changes: 89 additions & 0 deletions src/commands/docs-list.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
});
95 changes: 95 additions & 0 deletions src/commands/docs-view.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section extraction ignores fenced code blocks

High Severity

extractSection treats every line matching the heading regex as a real heading, including lines inside fenced code blocks. Developer documentation very commonly contains # comment lines in bash/shell/Python code blocks. When extracting a section, a # comment inside a code block is misidentified as a level-1 heading, triggering the level <= headingLevel termination condition and truncating the section prematurely. The function needs to track fenced code block state (``` delimiters) and skip heading detection inside them.

Fix in Cursor Fix in Web


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, "");
}
19 changes: 19 additions & 0 deletions src/commands/docs.ts
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",
},
},
});
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand All @@ -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",
Expand Down