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
22 changes: 13 additions & 9 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type * as Preset from '@docusaurus/preset-classic';
import type { Config } from '@docusaurus/types';

import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.mjs';
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.mjs';
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.mjs';
import remarkNpm2Yarn from './src/plugins/remark-npm2yarn.mjs';
import remarkRawMarkdown from './src/plugins/remark-raw-markdown.mjs';
import disableFullySpecified from './src/plugins/disable-fully-specified.ts';
import llmsTxt from './src/plugins/llms-txt.ts';
import ogImage from './src/plugins/og-image.ts';
import reactNavigationVersions from './src/plugins/react-navigation-versions.ts';
import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.ts';
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.ts';
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.ts';
import remarkNpm2Yarn from './src/plugins/remark-npm2yarn.ts';
import remarkRawMarkdown from './src/plugins/remark-raw-markdown.ts';
import darkTheme from './src/themes/react-navigation-dark';
import lightTheme from './src/themes/react-navigation-light';

Expand Down Expand Up @@ -131,10 +135,10 @@ const config: Config = {
},
} satisfies Preset.ThemeConfig,
plugins: [
'./src/plugins/disable-fully-specified.mjs',
'./src/plugins/react-navigation-versions.mjs',
['./src/plugins/llms-txt.mjs', { latestVersion }],
'./src/plugins/og-image.ts',
disableFullySpecified,
reactNavigationVersions,
[llmsTxt, { latestVersion }],
ogImage,
[
'@docusaurus/plugin-client-redirects',
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import dedent from 'dedent';
import assert from 'node:assert';
import { describe, test } from 'node:test';
import rehypeStaticToDynamic from '../src/plugins/rehype-static-to-dynamic.mjs';
import rehypeStaticToDynamic, {
type RehypeStaticToDynamicElement as Element,
type RehypeStaticToDynamicElementChild as ElementChild,
type RehypeStaticToDynamicRoot as Root,
type RehypeStaticToDynamicText as Text,
type RehypeStaticToDynamicTreeChild as TreeChild,
} from '../plugins/rehype-static-to-dynamic.ts';

/**
* Helper function to create a test tree structure
*/
function createTestTree(code) {
return {
function createTestTree(code: string): Root {
const tree: Root = {
type: 'root',
children: [
{
type: 'element',
tagName: 'pre',
properties: {},
children: [
{
type: 'element',
tagName: 'code',
properties: {},
data: { meta: 'static2dynamic' },
children: [
{
Expand All @@ -29,23 +37,25 @@ function createTestTree(code) {
},
],
};

return tree;
}

/**
* Helper function to extract the transformed code from the tree
*/
function extractTransformedCode(tree) {
function extractTransformedCode(tree: Root): string {
// After transformation, the tree should have TabItem elements
const tabsElement = tree.children[0];

if (!tabsElement || tabsElement.tagName !== 'Tabs') {
if (!isElement(tabsElement) || tabsElement.tagName !== 'Tabs') {
throw new Error('Expected Tabs element not found');
}

// Find the "Dynamic" tab
const dynamicTab = tabsElement.children.find(
(child) =>
child.type === 'element' &&
(child): child is Element =>
isElement(child) &&
child.tagName === 'TabItem' &&
child.properties?.value === 'dynamic'
);
Expand All @@ -56,26 +66,36 @@ function extractTransformedCode(tree) {

// Extract the code from the dynamic tab
const preElement = dynamicTab.children.find(
(child) => child.type === 'element' && child.tagName === 'pre'
(child): child is Element => isElement(child) && child.tagName === 'pre'
);

if (!preElement) {
throw new Error('Pre element not found in dynamic tab');
}

const codeElement = preElement.children.find(
(child) => child.type === 'element' && child.tagName === 'code'
(child): child is Element => isElement(child) && child.tagName === 'code'
);

if (!codeElement) {
throw new Error('Code element not found');
}

const textNode = codeElement.children.find((child) => child.type === 'text');
const textNode = codeElement.children.find(isTextNode);

return textNode?.value || '';
}

function isElement(
node: TreeChild | ElementChild | undefined
): node is Element {
return Boolean(node && node.type === 'element');
}

function isTextNode(node: ElementChild): node is Text {
return node.type === 'text';
}

describe('rehype-static-to-dynamic', () => {
test('basic screen transformation', async () => {
const input = dedent /* javascript */ `
Expand Down
1 change: 0 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function Home() {

return (
<Layout
// @ts-expect-error this are needed but don't exist in the type
title={siteConfig.title}
description={siteConfig.tagline}
wrapperClassName="full-width"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export default function disableFullySpecified(context, options) {
import type { LoadContext, Plugin } from '@docusaurus/types';

export default function disableFullySpecified(
_context: LoadContext,
_options: unknown
): Plugin {
return {
name: 'disable-fully-specified',
configureWebpack() {
Expand Down
162 changes: 127 additions & 35 deletions src/plugins/llms-txt.mjs → src/plugins/llms-txt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,103 @@
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import versions from '../../versions.json';
import type { LoadContext, Plugin } from '@docusaurus/types';
import versionsData from '../../versions.json';

type FrontMatterData = Record<string, string>;

type ParsedFrontMatter = {
data: FrontMatterData;
content: string;
};

type SidebarCategory = {
type: 'category';
label: string;
items: SidebarItem[];
};

type SidebarItem = string | SidebarCategory | Record<string, unknown>;

type FullDoc = {
title: string;
url: string;
content: string;
};

type LlmsTxtOptions = {
latestVersion?: string;
};

type SidebarProcessResult = {
content: string;
docs: FullDoc[];
};

const versionsSource: unknown = versionsData;

const versions = Array.isArray(versionsSource)
? versionsSource.filter(
(version): version is string => typeof version === 'string'
)
: [];

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function hasOwnProperty<T extends object, K extends PropertyKey>(
value: T,
key: K
): value is T & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(value, key);
}

function isSidebarItem(value: unknown): value is SidebarItem {
return typeof value === 'string' || isRecord(value);
}

function isSidebarCategory(item: SidebarItem): item is SidebarCategory {
if (!isRecord(item)) {
return false;
}

if (
!hasOwnProperty(item, 'type') ||
!hasOwnProperty(item, 'label') ||
!hasOwnProperty(item, 'items')
) {
return false;
}

return (
item.type === 'category' &&
typeof item.label === 'string' &&
Array.isArray(item.items)
);
}

function normalizeRootItems(value: unknown): SidebarItem[] {
if (Array.isArray(value)) {
return value.filter(isSidebarItem);
}

if (isRecord(value)) {
const normalized: SidebarCategory[] = [];

for (const [label, items] of Object.entries(value)) {
normalized.push({
type: 'category',
label,
items: Array.isArray(items) ? items.filter(isSidebarItem) : [],
});
}

return normalized;
}

return [];
}

/**
* Parses frontmatter from markdown content.
Expand All @@ -10,7 +106,7 @@ import versions from '../../versions.json';
* @param {string} fileContent - Raw markdown file content.
* @returns {{data: Object, content: string}} - Parsed data and stripped content.
*/
function parseFrontMatter(fileContent) {
function parseFrontMatter(fileContent: string): ParsedFrontMatter {
const frontMatterRegex = /^---\n([\s\S]+?)\n---\n/;
const match = fileContent.match(frontMatterRegex);

Expand All @@ -21,7 +117,7 @@ function parseFrontMatter(fileContent) {
const frontMatterBlock = match[1];
const content = fileContent.replace(frontMatterRegex, '');

const data = {};
const data: FrontMatterData = {};

frontMatterBlock.split('\n').forEach((line) => {
const parts = line.split(':');
Expand Down Expand Up @@ -59,15 +155,15 @@ function parseFrontMatter(fileContent) {
* @returns {{content: string, docs: Array}} - Generated index content and list of doc objects.
*/
function processSidebar(
items,
docsPath,
version,
isLatest,
baseUrl,
items: SidebarItem[],
docsPath: string,
version: string,
isLatest: boolean,
baseUrl: string,
level = 0
) {
): SidebarProcessResult {
let llmsContent = '';
let fullDocsList = [];
let fullDocsList: FullDoc[] = [];

items.forEach((item) => {
// Case 1: Item is a direct link (string ID)
Expand Down Expand Up @@ -99,7 +195,7 @@ function processSidebar(
}
}
// Case 2: Item is a category object
else if (item.type === 'category') {
else if (isSidebarCategory(item)) {
const label = item.label;
const headingPrefix = '#'.repeat(level + 3); // Start at level 3 (###)

Expand Down Expand Up @@ -128,13 +224,13 @@ function processSidebar(
* @returns {Array<string>} - List of generated filenames.
*/
function generateForVersion(
siteDir,
outDir,
version,
outputPrefix,
isLatest,
baseUrl
) {
siteDir: string,
outDir: string,
version: string,
outputPrefix: string,
isLatest: boolean,
baseUrl: string
): string[] {
const docsPath = path.join(siteDir, 'versioned_docs', `version-${version}`);
const sidebarPath = path.join(
siteDir,
Expand All @@ -147,25 +243,18 @@ function generateForVersion(
return [];
}

const sidebarConfig = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
const sidebarConfig: unknown = JSON.parse(
fs.readFileSync(sidebarPath, 'utf8')
);

// Handle different Docusaurus sidebar structures (root 'docs' key or first key)
let rootItems = sidebarConfig.docs || Object.values(sidebarConfig)[0] || [];

// Normalize object-style sidebars (older Docusaurus versions) to array format
if (!Array.isArray(rootItems) && typeof rootItems === 'object') {
const normalized = [];

for (const [label, items] of Object.entries(rootItems)) {
normalized.push({
type: 'category',
label: label,
items: items,
});
}
const rootItemsSource = isRecord(sidebarConfig)
? hasOwnProperty(sidebarConfig, 'docs')
? sidebarConfig.docs
: Object.values(sidebarConfig)[0]
: undefined;

rootItems = normalized;
}
const rootItems = normalizeRootItems(rootItemsSource);

const { content: sidebarContent, docs } = processSidebar(
rootItems,
Expand Down Expand Up @@ -213,7 +302,10 @@ function generateForVersion(
return [summaryFilename, fullFilename];
}

export default function (context, options) {
export default function llmsTxtPlugin(
context: LoadContext,
options: LlmsTxtOptions
): Plugin {
return {
name: 'llms.txt',
async postBuild({ siteDir, outDir }) {
Expand Down
Loading