feat: Add table of contents generation for docs pages
This commit is contained in:
parent
803da33f37
commit
2e692f7264
@ -1,3 +1,6 @@
|
|||||||
|
import type { readingTime } from "reading-time-estimator";
|
||||||
|
import type { TableOfContents } from "./remarkHeadingId";
|
||||||
|
|
||||||
import { createHandler } from "@universal-middleware/fastify";
|
import { createHandler } from "@universal-middleware/fastify";
|
||||||
import { telefuncHandler } from "./server/telefunc-handler";
|
import { telefuncHandler } from "./server/telefunc-handler";
|
||||||
import { vikeHandler } from "./server/vike-handler";
|
import { vikeHandler } from "./server/vike-handler";
|
||||||
@ -11,6 +14,23 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const root = __dirname;
|
const root = __dirname;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Vike {
|
||||||
|
interface PageContext {
|
||||||
|
exports: {
|
||||||
|
frontmatter?: Partial<{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
}>;
|
||||||
|
readingTime?: ReturnType<typeof readingTime>;
|
||||||
|
tableOfContents?: TableOfContents;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = Fastify();
|
const app = Fastify();
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import type { readingTime } from "reading-time-estimator";
|
|
||||||
|
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
@ -12,22 +10,6 @@ const root = __dirname;
|
|||||||
|
|
||||||
const pagesDir = `${root}/dist/client`;
|
const pagesDir = `${root}/dist/client`;
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Vike {
|
|
||||||
interface PageContext {
|
|
||||||
exports: {
|
|
||||||
frontmatter?: Partial<{
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
tags: string[];
|
|
||||||
}>;
|
|
||||||
readingTime?: ReturnType<typeof readingTime>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = Fastify();
|
const app = Fastify();
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
import type { JSXElement } from "solid-js";
|
import type { JSXElement } from "solid-js";
|
||||||
|
|
||||||
import { TableOfContents } from "@/partials/TableOfContents";
|
|
||||||
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
import { readingTime } from "reading-time-estimator";
|
import { clientOnly } from "vike-solid/clientOnly";
|
||||||
import { collectSections } from "@/libs/sections";
|
|
||||||
import { clock } from "solid-heroicons/outline";
|
import { clock } from "solid-heroicons/outline";
|
||||||
import { navigation } from "@/libs/navigation";
|
import { navigation } from "@/libs/navigation";
|
||||||
import { Prose } from "@/components/Prose";
|
import { Prose } from "@/components/Prose";
|
||||||
import { MDXProvider } from "solid-jsx";
|
import { MDXProvider } from "solid-jsx";
|
||||||
import { createSignal } from "solid-js";
|
|
||||||
import { Icon } from "solid-heroicons";
|
import { Icon } from "solid-heroicons";
|
||||||
|
|
||||||
type DocsLayoutProps = {
|
type DocsLayoutProps = {
|
||||||
children: JSXElement;
|
children: JSXElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TableOfContents = clientOnly(() =>
|
||||||
|
import("@/partials/TableOfContents").then((m) => m.TableOfContents),
|
||||||
|
);
|
||||||
|
|
||||||
export function DocsLayout(props: DocsLayoutProps) {
|
export function DocsLayout(props: DocsLayoutProps) {
|
||||||
const {
|
const {
|
||||||
exports: { frontmatter, readingTime },
|
exports: { frontmatter, readingTime },
|
||||||
@ -34,7 +35,7 @@ export function DocsLayout(props: DocsLayoutProps) {
|
|||||||
<PrevNextLinks />
|
<PrevNextLinks />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <TableOfContents tableOfContents={tableOfContents} /> */}
|
<TableOfContents fallback={null} />
|
||||||
</MDXProvider>
|
</MDXProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,100 +1,6 @@
|
|||||||
import type { Node } from "@markdoc/markdoc";
|
export type Section = {
|
||||||
|
|
||||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
|
||||||
|
|
||||||
interface HeadingNode extends Node {
|
|
||||||
type: "heading";
|
|
||||||
attributes: {
|
|
||||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
id?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type H2Node = HeadingNode & {
|
|
||||||
attributes: {
|
|
||||||
level: 2;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type H3Node = HeadingNode & {
|
|
||||||
attributes: {
|
|
||||||
level: 3;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function isHeadingNode(node: Node): node is HeadingNode {
|
|
||||||
return (
|
|
||||||
node.type === "heading" &&
|
|
||||||
[1, 2, 3, 4, 5, 6].includes(node.attributes.level) &&
|
|
||||||
(typeof node.attributes.id === "string" ||
|
|
||||||
typeof node.attributes.id === "undefined")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isH2Node(node: Node): node is H2Node {
|
|
||||||
return isHeadingNode(node) && node.attributes.level === 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isH3Node(node: Node): node is H3Node {
|
|
||||||
return isHeadingNode(node) && node.attributes.level === 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodeText(node: Node) {
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
for (const child of node.children ?? []) {
|
|
||||||
if (child.type === "text") {
|
|
||||||
text += child.attributes.content;
|
|
||||||
}
|
|
||||||
text += getNodeText(child);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Subsection = H3Node["attributes"] & {
|
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
children?: undefined;
|
level: 2 | 3;
|
||||||
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Section = H2Node["attributes"] & {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
children: Array<Subsection>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function collectSections(
|
|
||||||
nodes: Array<Node>,
|
|
||||||
slugify = slugifyWithCounter(),
|
|
||||||
) {
|
|
||||||
const sections: Array<Section> = [];
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (isH2Node(node) || isH3Node(node)) {
|
|
||||||
const title = getNodeText(node);
|
|
||||||
if (title) {
|
|
||||||
const id = slugify(title);
|
|
||||||
|
|
||||||
if (isH3Node(node)) {
|
|
||||||
if (!sections[sections.length - 1]) {
|
|
||||||
throw new Error(
|
|
||||||
"Cannot add `h3` to table of contents without a preceding `h2`",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sections[sections.length - 1].children.push({
|
|
||||||
...node.attributes,
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sections.push({ ...node.attributes, id, title, children: [] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sections.push(...collectSections(node.children ?? [], slugify));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|||||||
9
app/pages/+data.ts
Normal file
9
app/pages/+data.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { PageContext } from "vike/types";
|
||||||
|
|
||||||
|
export type Data = Awaited<ReturnType<typeof data>>;
|
||||||
|
|
||||||
|
export async function data(pageContext: PageContext) {
|
||||||
|
return {
|
||||||
|
tableOfContents: pageContext.exports.tableOfContents,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,37 +1,39 @@
|
|||||||
import type { Section, Subsection } from "@/libs/sections";
|
import type { TableOfContents as TableOfContentsType } from "@/remarkHeadingId";
|
||||||
|
import type { Section } from "@/libs/sections";
|
||||||
|
import type { Data } from "@/pages/+data";
|
||||||
|
|
||||||
import { createSignal, createEffect, For } from "solid-js";
|
import { createSignal, createEffect, For } from "solid-js";
|
||||||
|
import { useData } from "vike-solid/useData";
|
||||||
import { Link } from "@/components/Link";
|
import { Link } from "@/components/Link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
type TableOfContentsProps = {
|
export function TableOfContents() {
|
||||||
tableOfContents: Array<Section>;
|
const { tableOfContents } = useData<Data>();
|
||||||
};
|
|
||||||
|
if (!tableOfContents) return null;
|
||||||
|
|
||||||
export function TableOfContents(props: TableOfContentsProps) {
|
|
||||||
const [currentSection, setCurrentSection] = createSignal(
|
const [currentSection, setCurrentSection] = createSignal(
|
||||||
props.tableOfContents[0]?.id,
|
tableOfContents[0]?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHeadings = (tableOfContents: Array<Section>) => {
|
const getHeadings = () => {
|
||||||
return tableOfContents
|
return tableOfContents
|
||||||
.flatMap((node) => [node.id, ...node.children.map((child) => child.id)])
|
.map((section) => {
|
||||||
.map((id) => {
|
const el = document.getElementById(section.id);
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
|
|
||||||
const style = window.getComputedStyle(el);
|
const style = window.getComputedStyle(el);
|
||||||
const scrollMt = Number.parseFloat(style.scrollMarginTop);
|
const scrollMt = Number.parseFloat(style.scrollMarginTop);
|
||||||
|
|
||||||
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
|
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
|
||||||
return { id, top };
|
return { id: section.id, top };
|
||||||
})
|
})
|
||||||
.filter((x): x is { id: string; top: number } => x !== null);
|
.filter((x): x is { id: string; top: number } => x !== null);
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.tableOfContents.length === 0) return;
|
if (tableOfContents.length === 0) return;
|
||||||
const headings = getHeadings(props.tableOfContents);
|
const headings = getHeadings();
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
const top = window.scrollY;
|
const top = window.scrollY;
|
||||||
@ -49,69 +51,46 @@ export function TableOfContents(props: TableOfContentsProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("scroll", onScroll);
|
window.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
}, [getHeadings, props.tableOfContents]);
|
}, [getHeadings, tableOfContents]);
|
||||||
|
|
||||||
function isActive(section: Section | Subsection) {
|
function isActive(section: Section) {
|
||||||
if (section.id === currentSection()) return true;
|
if (section.id === currentSection()) return true;
|
||||||
if (!section.children) return false;
|
return false;
|
||||||
|
// if (!section.children) return false;
|
||||||
|
|
||||||
return section.children.findIndex(isActive) > -1;
|
// return section.children.findIndex(isActive) > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
<div class="hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
||||||
<nav aria-labelledby="on-this-page-title" class="w-56">
|
<nav aria-labelledby="on-this-page-title" class="w-56">
|
||||||
{props.tableOfContents.length > 0 && (
|
<h2
|
||||||
<>
|
id="on-this-page-title"
|
||||||
<h2
|
class="font-display text-sm font-medium text-slate-900"
|
||||||
id="on-this-page-title"
|
>
|
||||||
class="font-display text-sm font-medium text-slate-900"
|
Table des matières
|
||||||
>
|
</h2>
|
||||||
Table des matières
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ol class="mt-4 space-y-3 text-sm">
|
<ol class="mt-4 space-y-3 text-sm">
|
||||||
<For each={props.tableOfContents}>
|
<For each={tableOfContents}>
|
||||||
{(section) => (
|
{(section) => (
|
||||||
<li>
|
<li>
|
||||||
<h3>
|
<h3>
|
||||||
<Link
|
<Link
|
||||||
href={`#${section.id}`}
|
href={`#${section.id}`}
|
||||||
class={clsx(
|
class={clsx(
|
||||||
isActive(section)
|
isActive(section)
|
||||||
? "text-violet-500"
|
? "text-violet-500"
|
||||||
: "font-normal text-slate-500 hover:text-slate-700",
|
: "font-normal text-slate-500 hover:text-slate-700",
|
||||||
)}
|
|
||||||
>
|
|
||||||
{section.title}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
{section.children.length > 0 && (
|
|
||||||
<ol class="mt-2 space-y-3 pl-5 text-slate-500">
|
|
||||||
<For each={section.children}>
|
|
||||||
{(subSection) => (
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href={`#${subSection.id}`}
|
|
||||||
class={
|
|
||||||
isActive(subSection)
|
|
||||||
? "text-violet-500"
|
|
||||||
: "hover:text-slate-600"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{subSection.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ol>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
>
|
||||||
)}
|
{section.title}
|
||||||
</For>
|
</Link>
|
||||||
</ol>
|
</h3>
|
||||||
</>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,26 +1,18 @@
|
|||||||
import type { Root, Literal } from "mdast";
|
|
||||||
import type { Program } from "estree-jsx";
|
import type { Program } from "estree-jsx";
|
||||||
import type { Plugin } from "unified";
|
import type { Plugin } from "unified";
|
||||||
import type { VFile } from "vfile";
|
import type { VFile } from "vfile";
|
||||||
|
import type { Root } from "mdast";
|
||||||
|
|
||||||
import { readingTime } from "reading-time-estimator";
|
import { readingTime } from "reading-time-estimator";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
// Type pour le frontmatter
|
|
||||||
export interface Frontmatter {
|
export interface Frontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface pour le noeud YAML
|
|
||||||
interface YamlNode extends Literal {
|
|
||||||
type: "yaml";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface pour le noeud MDX ESM
|
|
||||||
interface MDXJSEsm {
|
interface MDXJSEsm {
|
||||||
type: "mdxjsEsm";
|
type: "mdxjsEsm";
|
||||||
value: string;
|
value: string;
|
||||||
@ -29,7 +21,6 @@ interface MDXJSEsm {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type pour la VFile avec données personnalisées
|
|
||||||
interface CustomVFile extends VFile {
|
interface CustomVFile extends VFile {
|
||||||
data: {
|
data: {
|
||||||
frontmatter?: Frontmatter;
|
frontmatter?: Frontmatter;
|
||||||
@ -40,7 +31,7 @@ interface CustomVFile extends VFile {
|
|||||||
|
|
||||||
const remarkExtractFrontmatter: Plugin<[], Root> =
|
const remarkExtractFrontmatter: Plugin<[], Root> =
|
||||||
() => (tree: Root, file: CustomVFile) => {
|
() => (tree: Root, file: CustomVFile) => {
|
||||||
visit(tree, "yaml", (node: YamlNode) => {
|
visit(tree, "yaml", (node) => {
|
||||||
try {
|
try {
|
||||||
const data = (yaml.load(node.value) as Frontmatter) || {};
|
const data = (yaml.load(node.value) as Frontmatter) || {};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { Heading, PhrasingContent } from "mdast";
|
import type { Heading, PhrasingContent } from "mdast";
|
||||||
|
import type { Section } from "./libs/sections";
|
||||||
|
import type { Program } from "estree-jsx";
|
||||||
import type { Plugin } from "unified";
|
import type { Plugin } from "unified";
|
||||||
import type { Root } from "mdast";
|
import type { Root } from "mdast";
|
||||||
|
|
||||||
@ -9,6 +11,18 @@ type PhrasingContentWithParent = PhrasingContent & {
|
|||||||
children: PhrasingContent[];
|
children: PhrasingContent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TableOfContents = Array<Section>;
|
||||||
|
|
||||||
|
interface MDXJSEsm {
|
||||||
|
type: "mdxjsEsm";
|
||||||
|
value: string;
|
||||||
|
data?: {
|
||||||
|
estree?: Program;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableOfContents: TableOfContents = [];
|
||||||
|
|
||||||
const doesHaveChildren = (child: PhrasingContent): boolean => {
|
const doesHaveChildren = (child: PhrasingContent): boolean => {
|
||||||
return ["delete", "emphasis", "strong", "link", "linkReference"].includes(
|
return ["delete", "emphasis", "strong", "link", "linkReference"].includes(
|
||||||
child.type,
|
child.type,
|
||||||
@ -40,32 +54,139 @@ const extractText = (children: PhrasingContent[]): string => {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
const remarkHeadingId: Plugin<[], Root> = () => (tree: Root) => {
|
const formatExportNode = (): MDXJSEsm => {
|
||||||
|
return {
|
||||||
|
type: "mdxjsEsm",
|
||||||
|
value: `export const tableOfContents = ${JSON.stringify(tableOfContents)};`,
|
||||||
|
data: {
|
||||||
|
estree: {
|
||||||
|
type: "Program",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "ExportNamedDeclaration",
|
||||||
|
declaration: {
|
||||||
|
type: "VariableDeclaration",
|
||||||
|
kind: "const",
|
||||||
|
declarations: [
|
||||||
|
{
|
||||||
|
type: "VariableDeclarator",
|
||||||
|
id: {
|
||||||
|
type: "Identifier",
|
||||||
|
name: "tableOfContents",
|
||||||
|
},
|
||||||
|
init: {
|
||||||
|
type: "ArrayExpression",
|
||||||
|
elements: tableOfContents.map((section) => ({
|
||||||
|
type: "ObjectExpression",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
type: "Property",
|
||||||
|
key: {
|
||||||
|
type: "Identifier",
|
||||||
|
name: "id",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: "Literal",
|
||||||
|
value: section.id,
|
||||||
|
},
|
||||||
|
kind: "init",
|
||||||
|
computed: false,
|
||||||
|
method: false,
|
||||||
|
shorthand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Property",
|
||||||
|
key: {
|
||||||
|
type: "Identifier",
|
||||||
|
name: "title",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: "Literal",
|
||||||
|
value: section.title,
|
||||||
|
},
|
||||||
|
kind: "init",
|
||||||
|
computed: false,
|
||||||
|
method: false,
|
||||||
|
shorthand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Property",
|
||||||
|
key: {
|
||||||
|
type: "Identifier",
|
||||||
|
name: "level",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: "Literal",
|
||||||
|
value: section.level,
|
||||||
|
},
|
||||||
|
kind: "init",
|
||||||
|
computed: false,
|
||||||
|
method: false,
|
||||||
|
shorthand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Property",
|
||||||
|
key: {
|
||||||
|
type: "Identifier",
|
||||||
|
name: "path",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: "Literal",
|
||||||
|
value: section.path,
|
||||||
|
},
|
||||||
|
kind: "init",
|
||||||
|
computed: false,
|
||||||
|
method: false,
|
||||||
|
shorthand: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
specifiers: [],
|
||||||
|
source: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sourceType: "module",
|
||||||
|
} as unknown as Program,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const remarkHeadingId: Plugin<[], Root> = () => (tree: Root, file) => {
|
||||||
const slugify = slugifyWithCounter();
|
const slugify = slugifyWithCounter();
|
||||||
|
|
||||||
visit(tree, "heading", (node) => {
|
visit(tree, "heading", (node) => {
|
||||||
const lastChild = node.children[node.children.length - 1];
|
const lastChild = node.children[node.children.length - 1];
|
||||||
|
|
||||||
|
const filePath = file.path;
|
||||||
|
console.log(`File path: ${filePath}`);
|
||||||
|
|
||||||
if (lastChild && lastChild.type === "text") {
|
if (lastChild && lastChild.type === "text") {
|
||||||
let string = lastChild.value.replace(/ +$/, "");
|
const string = lastChild.value.replace(/ +$/, "");
|
||||||
const matched = string.match(/ {#(.*?)}$/);
|
const matched = string.match(/ {#(.*?)}$/);
|
||||||
|
|
||||||
if (matched) {
|
if (matched) return;
|
||||||
const id = matched[1];
|
|
||||||
|
|
||||||
if (id.length > 0) {
|
|
||||||
setNodeId(node, id);
|
|
||||||
|
|
||||||
string = string.substring(0, matched.index);
|
|
||||||
lastChild.value = string;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slug = slugify(extractText(node.children));
|
const slug = slugify(extractText(node.children));
|
||||||
setNodeId(node, slug);
|
setNodeId(node, slug);
|
||||||
|
|
||||||
|
const depth = node.depth as 2 | 3;
|
||||||
|
if (depth > 3) return;
|
||||||
|
|
||||||
|
tableOfContents.push({
|
||||||
|
id: slug,
|
||||||
|
title: extractText(node.children),
|
||||||
|
level: depth,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportNode = formatExportNode();
|
||||||
|
tree.children.push(exportNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default remarkHeadingId;
|
export default remarkHeadingId;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user