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: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@types/hosted-git-info": "^3.0.5",
"@types/mdast": "^4.0.4",
"@types/node": "^24.0.14",
"@types/unist": "^2.0.0",
"@types/unist": "^3.0.0",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-n": "^17.18.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/-private/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import { visit } from 'unist-util-visit';
import { Node } from 'unist';
import type { Node } from 'unist';
import type { Root as MdastRoot } from 'mdast';
import { toString } from 'mdast-util-to-string';
import { slug } from 'github-slugger';
import url from 'url';
Expand Down Expand Up @@ -73,11 +74,10 @@ export function generateAutoUrl(source: string, prefix?: string, suffix?: string
return clearURL(parts, ignoreSuffix ? '' : suffix || '');
}

export function inferTitle(ast: Node): string | undefined {
export function inferTitle(ast: MdastRoot): string | undefined {
let docTitle: string | undefined;
visit(ast, 'heading', node => {
const { depth } = node as never;
if (depth !== 1) return;
if (node.depth !== 1) return;
docTitle = toString(node);
});
return docTitle;
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { getRepoEditUrl } from './-private/repo-info.js';
import { transformToNestedPageMetadata } from './-private/nested-page-metadata.js';
import debugFactory from 'debug';
import type { Root as MdastRoot } from 'mdast';
import type { Root as HastRoot } from 'hast';
const debug = debugFactory('@docfy/core');

class Docfy {
Expand Down Expand Up @@ -87,7 +88,10 @@ class Docfy {
reject(err);
} else {
resolve({
content: ctx.pages,
// The pipeline holds `Context<PageAST>` because `page.ast` changes
// shape half-way through it. By the time it resolves,
// `transformerMdastToHast` has replaced every tree with hast.
content: ctx.pages as PageContent<HastRoot>[],
staticAssets: ctx.staticAssets,
nestedPageMetadata: transformToNestedPageMetadata(
ctx.pages.map(p => p.meta),
Expand All @@ -101,6 +105,11 @@ class Docfy {
});
}

/*
* Turns every page's mdast tree into a hast tree. This is the point where
* `PageContent.ast` switches from `mdast.Root` to `hast.Root`, which is why
* the pipeline is typed with the union of both and narrows here.
*/
private transformerMdastToHast(ctx: Context): void {
ctx.pages.forEach(page => {
page.ast = ctx.rehype.runSync(page.ast as MdastRoot, page.vFile);
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/plugins/render-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import plugin from '../plugin.js';
import stringify from 'rehype-stringify';
import type { Root as HastRoot } from 'hast';

export default plugin({
runAfter(context): void {
Expand All @@ -9,9 +8,9 @@ export default plugin({
});

context.pages.forEach(page => {
page.rendered = rehype.stringify(page.ast as HastRoot);
page.rendered = rehype.stringify(page.ast);
page.demos?.forEach(demo => {
demo.rendered = rehype.stringify(demo.ast as HastRoot);
demo.rendered = rehype.stringify(demo.ast);
});
});
},
Expand Down
47 changes: 12 additions & 35 deletions packages/core/src/plugins/replace-internal-links.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import path from 'path';
import plugin from '../plugin.js';
import { Node } from 'unist';
import { visit } from 'unist-util-visit';
import { isValidUrl, isAnchorUrl } from '../-private/utils.js';
import { PageContent, Context } from '../types.js';
import type { Root as MdastRoot, Definition, Link } from 'mdast';

interface Resource {
url: string;
title?: string;
}
interface Association {
identifier: string;
label?: string;
}

interface LinkNode extends Node, Resource {
type: 'link';
}

interface LinkReferenceNode extends Node, Association {
type: 'linkReference';
}

interface DefinitionNode extends Node, Resource, Association {
type: 'definition';
}

function replaceURL(ctx: Context, page: PageContent, node: LinkNode | DefinitionNode): void {
function replaceURL(
ctx: Context<MdastRoot>,
page: PageContent<MdastRoot>,
node: Link | Definition
): void {
if (isValidUrl(node.url) || isAnchorUrl(node.url)) {
return;
}
Expand All @@ -51,25 +34,19 @@ function replaceURL(ctx: Context, page: PageContent, node: LinkNode | Definition
}
}

function isReferenceLink(node: LinkNode | LinkReferenceNode): node is LinkReferenceNode {
return node.type === 'linkReference';
}

function visitor(ctx: Context, page: PageContent): void {
const definitions: Record<string, DefinitionNode> = {};
function visitor(ctx: Context<MdastRoot>, page: PageContent<MdastRoot>): void {
const definitions: Record<string, Definition> = {};

visit(page.ast, 'definition', (node: DefinitionNode) => {
visit(page.ast, 'definition', node => {
definitions[node.identifier] = node;
});

visit(page.ast, ['link', 'linkReference'], visited => {
const node = visited as unknown as LinkNode | LinkReferenceNode;

if (isReferenceLink(node)) {
visit(page.ast, ['link', 'linkReference'], node => {
if (node.type === 'linkReference') {
if (definitions[node.identifier]) {
replaceURL(ctx, page, definitions[node.identifier]);
}
} else {
} else if (node.type === 'link') {
replaceURL(ctx, page, node);
}
});
Expand Down
41 changes: 7 additions & 34 deletions packages/core/src/plugins/static-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,8 @@ import { visit } from 'unist-util-visit';
import plugin from '../plugin.js';
import { PageContent } from '../types.js';
import { isValidUrl } from '../-private/utils.js';
import { Node } from 'unist';
import path from 'path';

interface Resource {
url: string;
title?: string;
}
interface Association {
identifier: string;
label?: string;
}

interface ImageReferenceNode extends Node, Association {
type: 'imageReference';
}

interface DefinitionNode extends Node, Resource, Association {
type: 'definition';
}

interface ImageNode extends Node, Resource {
type: 'image';
}

function isImageReference(node: ImageNode | ImageReferenceNode): node is ImageReferenceNode {
return node.type === 'imageReference';
}
import type { Root as MdastRoot, Definition, Image } from 'mdast';

function generateUniqueFileName(seen: string[], name: string, count?: number): string {
if (seen.indexOf(name) == -1) {
Expand Down Expand Up @@ -56,7 +31,7 @@ export default plugin({

const assets: Record<string, string> = {};

function transform(page: PageContent, node: DefinitionNode | ImageNode): void {
function transform(page: PageContent<MdastRoot>, node: Definition | Image): void {
if (!isValidUrl(node.url) && !path.isAbsolute(node.url)) {
const absolutePath = path.resolve(
path.join(page.sourceConfig.root, path.dirname(page.source)),
Expand All @@ -79,20 +54,18 @@ export default plugin({
}

ctx.pages.forEach(page => {
const definitions: Record<string, DefinitionNode> = {};
const definitions: Record<string, Definition> = {};

visit(page.ast, 'definition', (node: DefinitionNode) => {
visit(page.ast, 'definition', node => {
definitions[node.identifier] = node;
});

visit(page.ast, ['image', 'imageReference'], visited => {
const node = visited as unknown as ImageNode | ImageReferenceNode;

if (isImageReference(node)) {
visit(page.ast, ['image', 'imageReference'], node => {
if (node.type === 'imageReference') {
if (definitions[node.identifier]) {
transform(page, definitions[node.identifier]);
}
} else {
} else if (node.type === 'image') {
transform(page, node);
}
});
Expand Down
43 changes: 16 additions & 27 deletions packages/core/src/plugins/toc.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import plugin from '../plugin.js';
import { Heading } from '../types.js';
import { visit } from 'unist-util-visit';
import { Node, Parent } from 'unist';
import { toString } from 'mdast-util-to-string';
import { deleteNode } from '../-private/utils.js';

interface HeadingNode extends Node {
depth: number;
data: {
id: string;
docfyDelete?: boolean;
};
}
import type { Heading as HeadingNode } from 'mdast';

function getHeading(node: HeadingNode): Heading {
return {
title: toString(node),
id: node.data.id,
// `mdastSlug` runs on every tree before any plugin does, so `data.id` is
// always set by the time we get here. It is optional in the type because
// it is a Docfy augmentation of mdast's `HeadingData`.
id: node.data?.id as string,
depth: node.depth,
};
}
Expand All @@ -38,31 +33,25 @@ function findParentOfDepth(headings: Heading[], depth: number): Heading[] {
}
}

function isHeading(node: Node): node is HeadingNode {
return node.type === 'heading';
}

export default plugin({
runWithMdast(ctx): void {
ctx.pages.forEach((page): void => {
const headings: Heading[] = [];

visit(page.ast, (node: Node, _, parentNode: Parent | undefined) => {
if (isHeading(node)) {
if (node.depth === 1) {
return;
}
visit(page.ast, 'heading', (node, _, parentNode) => {
if (node.depth === 1) {
return;
}

if (node.depth > ctx.options.tocMaxDepth) {
return;
}
const parent = findParentOfDepth(headings, node.depth);
if (node.depth > ctx.options.tocMaxDepth) {
return;
}
const parent = findParentOfDepth(headings, node.depth);

parent.push(getHeading(node));
parent.push(getHeading(node));

if (node.data.docfyDelete && parentNode) {
deleteNode(parentNode.children, node);
}
if (node.data?.docfyDelete && parentNode) {
deleteNode(parentNode.children, node);
}
});

Expand Down
Loading
Loading