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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,36 @@ site: {

The theme renders these tags in the shared page `<head>`, including static output. Omit this option to emit no verification tags; consumer middleware is unnecessary.

Configure analytics using component names exported by [astro-analytics](https://github.com/Destiner/astro-analytics). Each value is passed directly as that component's props. Include only the services you use:
Top navigation supports links and two-level dropdown groups. Existing `{ text, href }` entries remain unchanged; use `{ text, children }` for a group. `children` must contain at least one link and cannot contain further groups or coexist with a parent `href`:

```js
navigation: [
{ text: { zh: "文档", en: "Docs" }, href: "/manual/" },
{
text: { zh: "工具", en: "Tools" },
children: [
{ text: { zh: "数据", en: "Data" }, href: "/tools/" },
{ text: { zh: "编辑器", en: "Editor" }, href: "https://editor.example.com/" },
],
},
],
```

All dropdown links are present in the initial HTML. Internal links receive the locale and Astro `base`; external links retain their URL and show a link icon. The matching child and its parent group are highlighted. Dropdowns open on desktop hover or summary activation, use inline disclosure on mobile, and support Tab, Enter/Space, arrow keys, Escape and outside-click dismissal. Only one header dropdown stays open at a time. Native disclosure remains usable without JavaScript; flat navigation, home actions and footer links keep their existing schema.

Configure analytics using the built-in `Baidu` adapter or component names exported by [astro-analytics](https://github.com/Destiner/astro-analytics). Each value is passed directly as that component's props. Include only the services you use:

```js
analytics: {
Baidu: { id: "0123456789abcdef0123456789abcdef" },
GoogleAnalytics: { id: "G-XXXXXXXXXX" },
SimpleAnalytics: {},
Matomo: { id: "1", url: "https://analytics.example.com/" },
},
```

`Baidu.id` is the 32-character hexadecimal site ID from the Baidu tracking script URL (`hm.js?<id>`). The theme initializes `window._hmt` before asynchronously loading that script.

Configured components render at the end of the body on all theme pages in production builds, including any `noscript` fallback they provide. Omit `analytics` to disable tracking; development builds never enable it. Unknown component names fail the build.

Add these scripts to `package.json`:
Expand Down Expand Up @@ -166,6 +186,8 @@ import { Demo } from '@antv/astro-theme-antv/components';
```

Provide either static `code` or a `src` path to a JavaScript or TypeScript file relative to `content.docs`.
Plain Markdown pages can reuse fragments with a standalone `<!-- include: ./shared/options.md -->` line. Paths are relative to the including file and must remain inside `content.docs`; nested includes are supported, fragment frontmatter is omitted, and cycles are rejected. HTML rendering and Markdown exports expand the same content. Use root-relative links and assets in shared fragments. Exclude fragment-only directories from `antvDocsLoader` with a negative glob, for example `pattern: ['**/*.{zh,en}.{md,mdx}', '!shared/**']`, so they do not create duplicate pages. Include directives inside code fences remain literal; MDX uses its normal component imports.

Demos run in an iframe using `srcdoc`, without a separate preview route; the parent page retains the complete source in build-time `<pre><code>` HTML and page-level Markdown, readable without JavaScript.

Demos use Sucrase to compile TypeScript and JSX in both the initial preview and editor runs, with the automatic JSX runtime by default (classic JSX pragmas are supported). They use native ES modules to load dependencies. Install packages used by your demos in the site. The theme discovers their imports automatically; use `demo.dependencies` only when overriding module resolution, for example:
Expand All @@ -188,7 +210,17 @@ Use normal relative imports for local JavaScript, TypeScript, JSX and JSON modul

Each example page contains only its own referenced local files in read-only source tabs, Markdown and export fields. Gallery navigation replaces these panels with those from the destination page; unrelated examples are not embedded in hidden HTML. StackBlitz exports preserve the entry's site-relative path and its transitive supporting files, so relative imports work unchanged; unrelated files and npm dependencies are excluded. The source tab remains editable, copying uses the selected file, and running/exporting uses the current entry source. Save new dependencies in the example source so they can be collected before using them in the browser editor or export. Computed dynamic import paths and non-module assets are not automatically collected.

Existing demos can also import standalone CSS or Less files (`import './index.less'`). Install `less` in the consuming site for Less compilation. Vite compiles these styles for both initial previews and editor reruns, including static builds; source tabs, Markdown and StackBlitz retain the original style files, and exported projects include the Less compiler when needed. Stylesheet-internal `@import` and `url()` resources are not automatically collected for export; keep these demo styles self-contained. This does not change the theme UI's Tailwind styling.
Existing demos can also import standalone CSS or Less files (`import './index.less'`). Install `less` in the consuming site for Less compilation. Vite compiles these styles for both initial previews and editor reruns, including static builds; source tabs, Markdown and StackBlitz retain the original style files, and exported projects include the Less compiler when needed. Stylesheet-internal `@import` and `url()` resources are not automatically collected for export; keep these demo styles self-contained. Styles imported transitively by demo package or workspace modules also stay attached to those modules when running native iframe previews. This does not change the theme UI's Tailwind styling.

Common iframe initialization belongs in `demo.setup`, not in every example source:

```js
demo: { setup: './src/demo/setup.ts' },
```

The local module must export a default function (which may be async) accepting `DemoSetupContext` (`container: HTMLElement`, `path: string`). `path` is the original site-relative source path with a leading slash. Each fresh gallery preview or inline Demo iframe awaits setup before loading the example; setup failures appear in the existing error panel. Use the path when some demos already own their layout. Without `setup`, existing behavior is unchanged.

The setup module and its transitive JS/TS/CSS dependencies are collected separately from example source tabs. Their complete source is included in Markdown without adding a separate runtime environment section to the source panel. StackBlitz projects retain these files and use a generated `demo-entry.js` to await setup before importing the editable example. Setup runs inside the iframe or exported project, never in the documentation page. Consumer-specific SDK registration and DOM belong in that module, not in the theme.

Keep `demo.dependencies` for deliberate module overrides or dependencies that cannot be inferred from source. The theme pre-optimizes discovered and configured package imports for iframe execution, respecting `optimizeDeps.exclude`; additional compatibility exceptions can still be configured through Vite. Existing explicit root-relative local mappings remain supported (with the corresponding Vite alias for initial gallery previews). Exports retain their physical source directories and include a Vite alias configuration when these mappings are used, so relative imports inside aliased modules keep working.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@antv/astro-theme-antv",
"version": "0.1.0-beta.13",
"version": "0.1.0-beta.14",
"description": "Astro integration for static open-source documentation and examples",
"license": "MIT",
"repository": {
Expand Down
5 changes: 5 additions & 0 deletions src/agent-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
import { basename, extname, resolve } from "node:path";
import type { AgentComponent } from "./compiler/config.js";
import { assertWithin, assertRealpathWithin } from "./util.js";
import { expandMarkdownIncludes } from './markdown-includes.js';

const processor = createProcessor();
const markdownProcessor = createProcessor({ format: "md" });
Expand All @@ -22,6 +23,7 @@ export function fencedCode(source: string, language = "text"): string {

interface Options {
filePath: string;
docsRoot?: string;
canonical: string;
components: Record<string, AgentComponent>;
renderCode?: (component: { name: string; code: string; src?: string }) => string | Promise<string>;
Expand All @@ -43,6 +45,9 @@ export async function serializeAgentMarkdown(
body: string,
options: Options
): Promise<string> {
if (options.docsRoot && options.filePath.endsWith('.md')) {
body = await expandMarkdownIncludes(body, options.filePath, options.docsRoot);
}
let tree: Node;
try {
tree = (
Expand Down
43 changes: 39 additions & 4 deletions src/compiler/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export interface LocalizedLink {
href: string;
}

export interface DemoSetupContext {
/** Site-relative source path, beginning with a slash. */
path: string;
container: HTMLElement;
}

export type NavigationItem = LocalizedLink | {
text: LocalizedText;
children: LocalizedLink[];
};

export type ContentComponent = string | { type: "link-card" };

export type AgentComponent =
Expand All @@ -33,7 +44,9 @@ export type AgentComponent =
}
| { type: "link-card" };

export type AnalyticsConfig = Record<string, Record<string, unknown>>;
export type AnalyticsConfig = Record<string, Record<string, unknown>> & {
Baidu?: { id: string };
};

export const homeSlotNames = [
"beforeHero",
Expand Down Expand Up @@ -78,7 +91,7 @@ export interface AntVSiteConfig {
agentComponents?: Record<string, AgentComponent>;
sidebar?: Record<string, LocalizedText & { order?: number }>;
};
navigation?: LocalizedLink[];
navigation?: NavigationItem[];
versions?: Record<string, string>;
analytics?: AnalyticsConfig;
llms?: {
Expand Down Expand Up @@ -135,6 +148,8 @@ export interface AntVSiteConfig {
/** Demo import names mapped to installed packages or consumer-relative modules, emitted as native ESM entries. */
dependencies?: Record<string, string>;
height?: number;
/** Consumer-relative module exporting an async setup(context) function. */
setup?: string;
};
/** @deprecated Configure Astro's standard `outDir` instead. */
output?: never;
Expand Down Expand Up @@ -175,7 +190,7 @@ export interface ResolvedSiteConfig {
agentComponents: Record<string, AgentComponent>;
sidebar: Record<string, LocalizedText & { order?: number }>;
};
navigation: LocalizedLink[];
navigation: NavigationItem[];
versions: Record<string, string>;
analytics: AnalyticsConfig;
llms: {
Expand Down Expand Up @@ -222,6 +237,7 @@ export interface ResolvedSiteConfig {
demo: {
dependencies: Record<string, string>;
height: number;
setup?: string;
};
}

Expand Down Expand Up @@ -377,10 +393,27 @@ const configSchema = z
.default({}),
sidebar: z.record(z.string(), sidebarItemSchema).default({}),
}),
navigation: z.array(linkSchema).default([]),
navigation: z.array(z.union([
linkSchema,
z.strictObject({
text: localizedTextSchema,
children: z.array(linkSchema).min(1),
}),
])).default([]),
versions: z.record(z.string(), safeHrefSchema).default({}),
analytics: z
.record(z.string().min(1), z.record(z.string(), z.unknown()))
.superRefine((services, context) => {
if (!Object.hasOwn(services, "Baidu")) return;
const result = z.strictObject({
id: z.string().regex(/^[a-f0-9]{32}$/i, "Expected a 32-character Baidu site ID."),
}).safeParse(services.Baidu);
if (!result.success) {
for (const issue of result.error.issues) {
context.addIssue({ ...issue, path: ["Baidu", ...issue.path] });
}
}
})
.default({}),
llms: z
.strictObject({
Expand Down Expand Up @@ -466,6 +499,8 @@ const configSchema = z
.strictObject({
dependencies: z.record(z.string().min(1), z.string().min(1)).default({}),
height: z.number().int().min(240).max(1200).default(480),
setup: z.string().regex(/^\.\/[^?#]+\.(?:ts|tsx|js|jsx|mjs|mts)$/, "demo.setup must be a site-relative JS/TS module beginning with ./.")
.refine((value) => !value.split('/').includes('..'), "demo.setup must stay inside the site root.").optional(),
})
.default({
height: 480,
Expand Down
5 changes: 1 addition & 4 deletions src/compiler/content.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { openGraphSchema } from "../open-graph.js";
import type { OpenGraphOptions } from "../open-graph.js";
import { readFile, readdir } from "node:fs/promises";
import { createRequire } from "node:module";
import matter from "gray-matter";
import { dirname, relative, resolve, sep } from "node:path";
import { z } from "zod";
import { assertRealpathWithin, assertWithin, pathKey } from "../util.js";
Expand All @@ -12,9 +12,6 @@ import type {
SiteLocale,
} from "./config.js";

const require = createRequire(import.meta.url);
const matter = require("gray-matter") as typeof import("gray-matter");

export interface DemoRecord {
key: string;
categorySlug: string;
Expand Down
5 changes: 3 additions & 2 deletions src/demo-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname, extname, relative, resolve } from 'node:path';
import type { SiteRegistry } from './compiler/content.js';
import { parseMatter } from './compiler/content.js';
import { serializeAgentMarkdown } from './agent-markdown.js';
import { demoImports, demoImportPath } from './demo-source.js';
import { demoImports, demoImportPath, demoSetupSource } from './demo-source.js';
import { assertWithin, assertRealpathWithin, pathKey } from './util.js';

export interface DemoFiles {
Expand Down Expand Up @@ -55,6 +55,7 @@ export async function collectDemoFiles(registry: SiteRegistry): Promise<DemoFile
if (/\.[cm]?[jt]sx?$/.test(file)) await visit(files[filename]!, file);
}
};
if (config.demo.setup) await visit(demoSetupSource(config.demo.setup), resolve(config.root, 'index.ts'));
for (const demo of registry.demos) await visit(demo.source, demo.sourcePath);
const walk = async (directory: string): Promise<void> => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
Expand All @@ -63,7 +64,7 @@ export async function collectDemoFiles(registry: SiteRegistry): Promise<DemoFile
else if (/\.mdx?$/.test(file)) {
const { content } = parseMatter(await readFile(file, 'utf8'), file);
await serializeAgentMarkdown(content, {
filePath: file, canonical: config.site.origin, components: config.content.agentComponents,
filePath: file, docsRoot: config.content.docs, canonical: config.site.origin, components: config.content.agentComponents,
renderCode: async ({ name, code, src }) => {
if (name !== 'Demo') return '';
// DocumentDemo resolves src beneath docs; inline code has the document as its base.
Expand Down
14 changes: 14 additions & 0 deletions src/demo-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,17 @@ export function referencedDemoPackages(source: string, path: string, files: Reco
if (Object.keys(files).some((name) => name.endsWith('.less'))) packages.add('less');
return [...packages];
}
/** Shared environment entry is independent of the editable example source. */
export function demoSetupPath(setup?: string): string | undefined {
return setup ? demoImportPath(setup, '/index.ts') : undefined;
}

export function demoSetupSource(setup?: string): string {
return setup ? `import ${JSON.stringify(setup)};` : '';
}

/** Keep a standalone export's environment sequencing identical to iframe runs. */
export function demoExportEntry(path: string, setup?: string, sourcePath = path): string {
const filename = path.replace(/^\//, '');
return `${setup ? `const { default: setup } = await import(${JSON.stringify(`.${setup}`)});\nawait setup({container: document.getElementById('container'), path: ${JSON.stringify(sourcePath)}});\n` : ''}await import(${JSON.stringify(`./${filename}`)});`;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export const defineConfig = <const Config extends AntVSiteConfig>(

export type {
AnalyticsConfig,
DemoSetupContext,
AntVSiteConfig,
HomeSlotName,
LocalizedLink,
NavigationItem,
LocalizedText,
SiteLocale,
} from "./compiler/config.js";
Expand Down
36 changes: 36 additions & 0 deletions src/markdown-includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFile, realpath } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { parseMatter } from './compiler/content.js';
import { assertRealpathWithin, assertWithin } from './util.js';

/** Expand Markdown fragments without creating duplicate public documentation pages. */
export async function expandMarkdownIncludes(
markdown: string,
file: string,
root: string,
ancestors: string[] = [],
): Promise<string> {
if (!markdown.includes('<!-- include:')) return markdown;
root = await realpath(root);
const path = await realpath(resolve(file));
if (ancestors.includes(path)) throw new Error(`Circular Markdown include: ${[...ancestors, path].join(' -> ')}`);
const output: string[] = [];
let fence: { character: string; length: number } | undefined;
for (const line of markdown.split('\n')) {
const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
if (marker) {
if (!fence) fence = { character: marker[1]![0]!, length: marker[1]!.length };
else if (marker[1]![0] === fence.character && marker[1]!.length >= fence.length && !marker[2]!.trim()) fence = undefined;
output.push(line);
continue;
}
const include = !fence && line.match(/^<!-- include: (\.[^\r\n]+\.md) -->$/);
if (!include) { output.push(line); continue; }
const target = resolve(dirname(path), include[1]!);
assertWithin(root, target);
await assertRealpathWithin(root, target);
const { content } = parseMatter(await readFile(target, 'utf8'), target);
output.push(await expandMarkdownIncludes(content, target, root, [...ancestors, path]));
}
return output.join('\n');
}
3 changes: 2 additions & 1 deletion src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SiteLocale,
} from "./compiler/config.js";
import { assertRealpathWithin, assertWithin, isWithin, pathKey } from "./util.js";
import { expandMarkdownIncludes } from './markdown-includes.js';

const escapeHtml = (value: unknown) =>
String(value)
Expand Down Expand Up @@ -254,7 +255,7 @@ async function prepareLegacyContent(
base: string,
): Promise<string> {
const componentsExpanded = expandConfiguredComponents(
markdown,
await expandMarkdownIncludes(markdown, markdownPath, config.content.docs),
config.content.components,
locale,
base,
Expand Down
2 changes: 1 addition & 1 deletion src/theme/components/Announcement.astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { locale } = Astro.props;
>
<aside aria-label={locale === 'zh' ? '公告' : 'Announcement'}>
<span data-announcement-title class="[overflow-wrap:anywhere]"></span>
{' '}<a data-announcement-link hidden class="[&[hidden]]:hidden text-[#1677ff] hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)] [overflow-wrap:anywhere]"></a>
{' '}<a data-announcement-link hidden class="[&[hidden]]:hidden text-[#0969da] hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)] [overflow-wrap:anywhere]"></a>
<button
type="button"
data-announcement-close
Expand Down
Loading
Loading