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
2 changes: 2 additions & 0 deletions src/components/TabsScript.astro
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@

// Check if the code content is tall enough to collapse
if (scrollArea.scrollHeight <= CODE_COLLAPSE_HEIGHT) return;
// Highlighted lines are the point of the block, so never hide them.
if (block.querySelector('.shiki.has-highlighted')) return;

// Mark as collapsed
block.setAttribute('data-code-collapsed', 'true');
Expand Down
10 changes: 5 additions & 5 deletions src/components/docs/ExampleLinkBar.astro
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
// "Prefer to read code?" bar with a View-on-GitHub button, extracted from the
// inline HTML in the agentOS quickstart so every product's quickstart can use it.
// "See the full example on GitHub" bar with a View-on-GitHub button. Every
// docs page and guide that has a complete example links it with this bar, so
// the wording stays the same everywhere.
interface Props {
href: string;
label?: string;
}
const { href, label = "Prefer to read code? Clone the example repository." } = Astro.props;
const { href } = Astro.props;
---

<div
style="border-radius:0.75rem;border:1px solid rgb(var(--site-ink, 27 25 22) / 0.12);background:rgb(var(--site-ink, 27 25 22) / 0.035);padding:0.875rem 1.125rem;margin:1.5rem 0;color:rgb(var(--site-ink-soft, 86 82 74));display:flex;align-items:center;justify-content:space-between;gap:1.25rem;"
>
<span>{label}</span>
<span>See the full example on GitHub.</span>
<a
href={href}
style="appearance:none;border:1px solid rgb(var(--site-ink, 27 25 22) / 0.18);background:transparent;color:rgb(var(--site-ink, 27 25 22));font-family:var(--sl-font);font-size:0.8rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;height:2rem;padding:0 0.85rem;border-radius:6px;cursor:pointer;white-space:nowrap;text-decoration:none;flex:none;gap:0.45rem;box-sizing:border-box;"
Expand Down
34 changes: 32 additions & 2 deletions src/mdx/rehype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { toString as mdastToString } from "mdast-util-to-string";
import { mdxAnnotations } from "mdx-annotations";
import rehypeMermaid from "rehype-mermaid";
import * as shiki from "shiki";
import type { ShikiTransformer } from "shiki";
import { visit } from "unist-util-visit";
import theme from "../lib/textmate-code-theme";
import { repoForContentPath } from "../sitemap/docs-sources.node";
Expand Down Expand Up @@ -93,8 +94,14 @@ function rehypeParseCodeBlocks() {
const fileLabel = sourceFile
? sourceFile.split("/").pop()
: fileVal;
const highlightMatch = trimmed.match(
/\bhighlight="([0-9,-]+)"/,
);
if (highlightMatch) {
parentNode.properties.highlightLines = highlightMatch[1];
}
const rest = trimmed.replace(
/\b(?:title|file|region)=(?:"[^"]*"|'[^']*')/g,
/\b(?:title|file|region|highlight)=(?:"[^"]*"|'[^']*')/g,
" ",
);

Expand Down Expand Up @@ -198,6 +205,26 @@ export async function highlightDocsCode(code: string, lang: string): Promise<str
});
}

/** Marks the lines in `ranges` (such as `3-7,12`) highlighted and dims the rest. */
function transformerHighlightLines(ranges: unknown): ShikiTransformer {
const lines = new Set<number>();
if (typeof ranges === "string") {
for (const range of ranges.split(",")) {
const [start, end = start] = range.split("-").map(Number);
for (let line = start; line <= end; line++) lines.add(line);
}
}
return {
name: "highlight-lines",
pre(node) {
if (lines.size > 0) this.addClassToHast(node, "has-highlighted");
},
line(node, line) {
if (lines.has(line)) this.addClassToHast(node, "highlighted");
},
};
}

function rehypeShiki() {
return async (tree) => {
await docsHighlighter();
Expand Down Expand Up @@ -236,7 +263,10 @@ function rehypeShiki() {
const result = highlighter.codeToHtml(textNode.value, {
lang,
themes: { light: theme.name, dark: 'github-dark' },
transformers: [transformerNotationFocus()],
transformers: [
transformerNotationFocus(),
transformerHighlightLines(node.properties.highlightLines),
],
});
// Store the highlighted HTML in a property instead of the text node
// This prevents MDX from interpreting the HTML as JSX
Expand Down
72 changes: 70 additions & 2 deletions src/mdx/remark-code-snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { resolve, extname } from "node:path";
* `// docs:start name` … `// docs:end name` (markers are stripped, region is
* dedented).
*
* Highlight regions with `highlight="name"` (or `highlight="a,b"`): the snippet
* still shows the whole file (or `region`), and every other line is dimmed so
* the reader sees where the highlighted code sits in context.
*
* Paths are relative to the repo that owns the page. Product docs are symlinked
* in from their own repos, so an agentOS page's `examples/...` means agentOS's
* examples; the caller supplies a resolver that maps the MDX file's path to the
Expand Down Expand Up @@ -96,7 +100,8 @@ export function loadSnippet(
root: string,
file: string,
region?: string,
): { lang: string; value: string } {
highlight?: string,
): { lang: string; value: string; highlightLines?: string } {
const abs = resolve(root, file);
let source: string;
try {
Expand All @@ -110,9 +115,68 @@ export function loadSnippet(
return {
lang: LANG_BY_EXT[extname(abs).toLowerCase()] ?? "text",
value: extractRegion(source, region, file),
highlightLines: highlight
? highlightedLines(source, region, highlight, file)
: undefined,
};
}

/**
* The 1-based line numbers, as ranges like `3-7,12-14`, that the regions in
* `highlight` occupy in the code `extractRegion` returns for `region`.
*/
function highlightedLines(
source: string,
region: string | undefined,
highlight: string,
file: string,
): string {
const names = new Set(
highlight
.split(",")
.map((name) => name.trim())
.filter(Boolean),
);
const found = new Set<string>();
const open = new Set<string>();
const ranges: Array<[number, number]> = [];
// With `region`, numbering starts inside that region.
let shown = region === undefined;
let lineNumber = 0;

for (const line of source.split("\n")) {
const m = line.match(MARKER);
if (m) {
const [, kind, name] = m;
if (name === region) shown = kind === "start";
if (name && names.has(name)) {
if (kind === "start") {
open.add(name);
found.add(name);
} else {
open.delete(name);
}
}
continue;
}
if (!shown) continue;
lineNumber += 1;
if (open.size === 0) continue;
const last = ranges.at(-1);
if (last && last[1] === lineNumber - 1) last[1] = lineNumber;
else ranges.push([lineNumber, lineNumber]);
}

for (const name of names) {
if (!found.has(name)) {
throw new Error(
`<CodeSnippet>: highlight region "${name}" not found in ${file}`,
);
}
}
return ranges.map(([a, b]) => (a === b ? `${a}` : `${a}-${b}`)).join(",");
}

function getAttr(
node: {
attributes?: Array<{ type?: string; name?: string; value?: unknown }>;
Expand Down Expand Up @@ -168,13 +232,17 @@ export function remarkCodeSnippet(resolveRoot?: RootResolver) {
const region = getAttr(el, "region");
const title = getAttr(el, "title");
const lang = getAttr(el, "lang");
const highlight = getAttr(el, "highlight");

const snippet = loadSnippet(embedRoot, file, region);
const snippet = loadSnippet(embedRoot, file, region, highlight);

// `file=` in the meta drives the basename tab label + GitHub source
// link in the Code component; `title=` overrides the label.
const meta = [`file="${file}"`];
if (title) meta.push(`title="${title}"`);
if (snippet.highlightLines) {
meta.push(`highlight="${snippet.highlightLines}"`);
}

parent.children[index] = {
type: "code",
Expand Down
48 changes: 44 additions & 4 deletions src/sitemap/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,47 @@ function productSidebars(meta: ProductMetadata) {
const bundle = bundleSidebars(meta.id);
// A shared bundle's sidebar is authored against its source product's routes
// (`/agentos/docs/...`); serve it under this product's instead.
const ownDocs = mirrorGuides(bundle.docs, bundle.guides ?? []);
const docs = meta.bundleOf
? rerootPrefix(bundle.docs, `/${meta.bundleOf}/docs`, `/${meta.id}/docs`)
: bundle.docs;
? rerootPrefix(ownDocs, `/${meta.bundleOf}/docs`, `/${meta.id}/docs`)
: ownDocs;
return {
docs,
integrations: bundle.integrations ?? [],
};
}

/**
* Fills a `"mirror": "guides"` group in a bundle's docs sidebar with the
* bundle's own guides, so a docs tab can list them without repeating them:
*
* { "title": "Guides", "collapsible": true, "mirror": "guides" }
*
* The pages keep their `/guides/...` hrefs, so they open on the Guides tab.
*/
function mirrorGuides<T>(node: T, guides: SidebarItem[]): T {
if (Array.isArray(node)) {
return node.map((item) => mirrorGuides(item, guides)) as unknown as T;
}
if (!node || typeof node !== "object") return node;
const { mirror, ...rest } = node as Record<string, unknown>;
if (mirror === undefined) {
return Object.fromEntries(
Object.entries(rest).map(([key, value]) => [
key,
mirrorGuides(value, guides),
]),
) as T;
}
if (mirror !== "guides") {
throw new Error(`sidebar.json: unknown "mirror" value ${JSON.stringify(mirror)}`);
}
return {
...rest,
pages: guides.flatMap((group) => ("pages" in group ? group.pages : [group])),
} as T;
}

function rerootPrefix<T>(node: T, from: string, to: string): T {
if (Array.isArray(node)) {
return node.map((item) => rerootPrefix(item, from, to)) as unknown as T;
Expand Down Expand Up @@ -146,10 +178,13 @@ export function deploySidebar(): SidebarItem[] {
];
}

/** Guides tab groups in sidebar order, after General. Groups not listed follow in product order. */
const GUIDE_GROUP_ORDER = ["Architecture", "Agents", "Guides"];

/**
* The Guides tab's sidebar: the website's overview, then every bundle's
* `guides` groups in product order. Groups with the same title merge into one,
* at the position the title first appears, keeping each bundle's page order.
* `guides` groups, ordered by `GUIDE_GROUP_ORDER`. Groups with the same title
* merge into one, keeping each bundle's page order.
*/
export function guidesSidebar(): SidebarItem[] {
const overview: SidebarSection = {
Expand All @@ -169,6 +204,11 @@ export function guidesSidebar(): SidebarItem[] {
else merged.push({ ...item, pages: [...item.pages] });
}
}
const rank = (group: SidebarSection) => {
const index = GUIDE_GROUP_ORDER.indexOf(group.title);
return index === -1 ? GUIDE_GROUP_ORDER.length : index;
};
merged.sort((a, b) => rank(a) - rank(b));
return [overview, ...merged];
}

Expand Down
4 changes: 4 additions & 0 deletions src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
@apply opacity-100;
}

.shiki.has-highlighted .line:not(.highlighted) {
@apply opacity-40;
}

@layer utilities {
/* Cursor-following border glow for porcelain pills (deploy row, stack chips).
Lights ONLY the pill's 1px border ring as the pointer moves — not the fill.
Expand Down
Loading