diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 22672493..d184eb23 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,7 +1,7 @@ import { defineConfig } from 'vitepress'; import { transformerTwoslash } from 'vitepress-plugin-twoslash'; import { getRegisteredMarkdownTheme } from '../shared/utils'; -import { builder } from '../shared/multipleSidebarBuilder'; +import { sidebarService } from '../services/SidebarService'; // https://vitepress.dev/reference/site-config export default defineConfig({ @@ -33,7 +33,7 @@ export default defineConfig({ { text: 'Contact', link: '../contact.md' }, ], logo: '/favicon.ico', - sidebar: builder.emitSidebar(), + sidebar: sidebarService.getMultipleSidebar(), outline: { level: 'deep', }, diff --git a/docs/data/global.data.ts b/docs/data/global.data.ts index f939665d..ccd659f3 100644 --- a/docs/data/global.data.ts +++ b/docs/data/global.data.ts @@ -1,21 +1,5 @@ import { version } from '../../node_modules/vitepress/package.json'; -export const documentMap = { - 'Csharp Design Patterns': '👾', - 'Modern CSharp': '🐱‍👤', - Articles: '📰', - Avalonia: '😱', - Docker: '🐋', - Git: '🐱', - JavaScript: '😅', - SQL: '📝', - TypeScript: '⌨', - VBA: '💩', - Vue3: '⚡', -} as const; -export type DocumentName = keyof typeof documentMap; -export type DocumentIcon = (typeof documentMap)[keyof typeof documentMap]; - const globalData = { vitepressVersion: version, } as const; diff --git a/docs/services/DocumentService.ts b/docs/services/DocumentService.ts new file mode 100644 index 00000000..9169aca1 --- /dev/null +++ b/docs/services/DocumentService.ts @@ -0,0 +1,45 @@ +import { DocumentName, documentMap } from '../data/global.data'; +import * as File from '../shared/FileSystem'; +import { IDocumentService } from './IDocumentService'; + +class DocumentService implements IDocumentService { + getDocumentEntryFolder(name: DocumentName): File.DirectoryInfo { + const ret = this.registeredDocumentFolders().find(x => x.name === name); + if (!ret) throw new Error(`Document entry of ${name} not found.`); + return ret; + } + registeredDocumentFolders(): File.DirectoryInfo[] { + return this.documentSrc.getDirectories().filter(x => Object.keys(documentMap).includes(x.name)); + } + physicalDocumentFolders(): File.DirectoryInfo[] { + return this.documentSrc.getDirectories(); + } + getMarkdownEntryFolder(name: DocumentName): File.DirectoryInfo { + const ret = this.getDocumentEntryFolder(name) + .getDirectories() + .find(x => x.name === 'docs'); + if (!ret) throw new Error(`Markdown entry of ${name} not found.`); + return ret; + } + registeredCount(): number { + return Object.keys(documentMap).length; + } + physicalCount(): number { + return this.documentSrc.getDirectories().length; + } + physicalCountBy(f: (x: File.DirectoryInfo) => boolean): number { + return this.documentSrc.getDirectories().filter(x => f(x)).length; + } + getIndexLinkOfDocument(name: DocumentName): string { + throw new Error('Method not implemented.'); + } + get documentSrc(): File.DirectoryInfo { + const ret = File.projectRoot() + .getDirectories() + .find(x => x.name === 'document'); + if (!ret) throw new Error('Document source not found.'); + return ret; + } +} + +export const documentService: IDocumentService = new DocumentService(); diff --git a/docs/services/IDocumentService.ts b/docs/services/IDocumentService.ts new file mode 100644 index 00000000..c40b7e88 --- /dev/null +++ b/docs/services/IDocumentService.ts @@ -0,0 +1,28 @@ +import * as File from '../shared/FileSystem'; + +export interface IDocumentService { + physicalCount(): number; + registeredCount(): number; + physicalCountBy(f: (x: File.DirectoryInfo) => boolean): number; + getIndexLinkOfDocument(name: DocumentName): string; + get documentSrc(): File.DirectoryInfo; + getMarkdownEntryFolder(name: DocumentName): File.DirectoryInfo; + getDocumentEntryFolder(name: DocumentName): File.DirectoryInfo; + registeredDocumentFolders(): File.DirectoryInfo[]; + physicalDocumentFolders(): File.DirectoryInfo[]; +} +export const documentMap = { + 'Csharp Design Patterns': '👾', + 'Modern CSharp': '🐱‍👤', + Articles: '📰', + Avalonia: '😱', + Docker: '🐋', + Git: '🐱', + JavaScript: '😅', + SQL: '📝', + TypeScript: '⌨', + VBA: '💩', + Vue3: '⚡', +} as const; +export type DocumentName = keyof typeof documentMap; +export type DocumentIcon = (typeof documentMap)[keyof typeof documentMap]; diff --git a/docs/services/ISidebarService.ts b/docs/services/ISidebarService.ts new file mode 100644 index 00000000..56d94ed0 --- /dev/null +++ b/docs/services/ISidebarService.ts @@ -0,0 +1,13 @@ +import { DefaultTheme } from 'vitepress'; +import { DocumentName } from './IDocumentService'; +import * as File from '../shared/FileSystem'; +import { IDocumentService } from './IDocumentService'; +export interface ISidebarService { + readonly documentService: IDocumentService; + getMultipleSidebar(): DefaultTheme.SidebarMulti; + getSidebarOfDocument(name: DocumentName): DefaultTheme.SidebarItem[]; + transformFolderToSidebarItem( + folder: File.DirectoryInfo, + baseLink: string + ): DefaultTheme.SidebarItem[]; +} diff --git a/docs/services/IThemeService.ts b/docs/services/IThemeService.ts new file mode 100644 index 00000000..ae8ed6f4 --- /dev/null +++ b/docs/services/IThemeService.ts @@ -0,0 +1,7 @@ +import { themes } from '../../.github/workflows/beforeBuild/sync-themes.mjs'; +export type ThemeName = keyof typeof themes; +export interface IThemeService { + register(): void; + getTheme(name: ThemeName): any; + themes(): any[]; +} diff --git a/docs/services/SidebarService.ts b/docs/services/SidebarService.ts new file mode 100644 index 00000000..ca7e9458 --- /dev/null +++ b/docs/services/SidebarService.ts @@ -0,0 +1,72 @@ +import Enumerable from 'linq'; +import { DefaultTheme } from 'vitepress'; +import { DocumentName, documentMap } from './IDocumentService'; +import { DirectoryInfo, FileInfo, Path, documentRoot } from '../shared/FileSystem'; +import { documentService } from './DocumentService'; +import { IDocumentService } from './IDocumentService'; +import { ISidebarService } from './ISidebarService'; + +class SidebarService implements ISidebarService { + private readonly base: string = `/${documentRoot().name}`; + readonly documentService: IDocumentService = documentService; + getMultipleSidebar(): DefaultTheme.SidebarMulti { + let sidebar: DefaultTheme.SidebarMulti = {}; + for (const name of Object.keys(documentMap)) { + sidebar[`${this.base}/${name}/docs/`] = this.getSidebarOfDocument(name as DocumentName); + } + return sidebar; + } + getSidebarOfDocument(name: DocumentName): DefaultTheme.SidebarItem[] { + const markdownEntry = this.documentService.getMarkdownEntryFolder(name as DocumentName); + return [ + { + text: name, + items: this.transformFolderToSidebarItem(markdownEntry, `${this.base}/${name}`), + }, + ]; + } + transformFolderToSidebarItem(folder: DirectoryInfo, base: string): DefaultTheme.SidebarItem[] { + const subs = folder.getDirectories(); + // load files in this folder + let items: DefaultTheme.SidebarItem[] = folder.getFiles().length + ? filesToSidebarItems(folder.getFiles(), `${base}/${folder.name}`) + : []; + for (const index in subs) { + if (Object.prototype.hasOwnProperty.call(subs, index)) { + const sub = subs[index]; + const currentSidebarItem: DefaultTheme.SidebarItem = { + collapsed: false, + text: sub.name.replace(/^\d+\.\s*/, ''), // remove leading index + items: this.transformFolderToSidebarItem(sub, `${base}/${folder.name}`), + }; + items.push(currentSidebarItem); + } + } + return items; + function filesToSidebarItems(files: FileInfo[], base: string): DefaultTheme.SidebarItem[] { + return files + .map(file => { + const link = `${base}/${file.name}`; + return { + text: Path.GetFileNameWithoutExtension(file.name), + link: link.substring(0, link.lastIndexOf('.')), + }; + }) + .sort((x, y) => { + // if (!/^\d+\.\s*/.test(x.text) || !/^\d+\.\s*/.test(y.text)) + // throw new Error( + // `Files:\n${Enumerable.from(files) + // .select(f => f.fullName) + // .aggregate( + // (prev, current) => `${prev},\n${current}\n` + // )} don't have consistent leading indices.` + // ); + return ( + parseInt(x.text.match(/^\d+\.\s*/)?.[0]!) - parseInt(y.text.match(/^\d+\.\s*/)?.[0]!) + ); + }); + } + } +} + +export const sidebarService: ISidebarService = new SidebarService(); diff --git a/docs/data/services.data.ts b/docs/services/ThemeService.ts similarity index 100% rename from docs/data/services.data.ts rename to docs/services/ThemeService.ts diff --git a/docs/shared/multipleSidebarBuilder.ts b/docs/shared/multipleSidebarBuilder.ts deleted file mode 100644 index 5cdb4503..00000000 --- a/docs/shared/multipleSidebarBuilder.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DefaultTheme } from 'vitepress'; -import { DirectoryInfo, FileInfo, Path, documentRoot } from '../shared/FileSystem'; -import { folderToSidebarItems } from './utils'; -const docRoot = documentRoot(); -export const builder = { - base: `/${docRoot.name}` as string | undefined, - sidebar: {} as DefaultTheme.SidebarMulti, - rewrites: {} as Record, - registerSidebarAuto: function () { - if (!this.base) throw new Error('base not set'); - const docFolders = docRoot.getDirectories(); - for (const index in docFolders) { - if (Object.prototype.hasOwnProperty.call(docFolders, index)) { - const docParent = docFolders[index]; - const docs = docParent.getDirectories().find(d => d.name === 'docs'); - if (!docs) throw new Error(`doc folder for ${docParent.name} not found`); - const current: DefaultTheme.SidebarItem[] = [ - { - text: docParent.name, - items: folderToSidebarItems(docs, `${this.base}/${docParent.name}`), - }, - ]; - this.sidebar[`${this.base}/${docParent.name}/docs/`] = current; - } - } - return this; - }, - emitSidebar: function (): DefaultTheme.SidebarMulti { - return this.sidebar; - }, -}; -builder.registerSidebarAuto(); diff --git a/docs/shared/utils.ts b/docs/shared/utils.ts index 4374e791..319de04d 100644 --- a/docs/shared/utils.ts +++ b/docs/shared/utils.ts @@ -1,46 +1,20 @@ import * as fs from 'fs'; import path from 'path'; import * as shikiji from 'shikiji'; -import { DefaultTheme } from 'vitepress'; import { themes } from '../../.github/workflows/beforeBuild/sync-themes.mjs'; -import { FileInfo, Path, DirectoryInfo, projectRoot } from './FileSystem'; +import { projectRoot } from './FileSystem'; -export async function getRegisteredMarkdownTheme(theme: keyof typeof themes): Promise { - let isThemeRegistered = (await shikiji.getSingletonHighlighter()).getLoadedThemes().find(x => x === theme); - if (!isThemeRegistered) { - const myTheme = JSON.parse(fs.readFileSync(path.join(projectRoot().fullName, `public/${theme}.json`), 'utf8')); - (await shikiji.getSingletonHighlighter()).loadTheme(myTheme); - } - return (await shikiji.getSingletonHighlighter()).getTheme(theme); -} - -export function filesToSidebarItems(files: FileInfo[], base: string): DefaultTheme.SidebarItem[] { - return files.map(file => { - const link = `${base}/${file.name}`; - return { - text: Path.GetFileNameWithoutExtension(file.name), - link: link.substring(0, link.lastIndexOf('.')), - }; - }); -} - -export function folderToSidebarItems(folder: DirectoryInfo, base: string): DefaultTheme.SidebarItem[] { - if (!folder.exists) throw new Error(`folder: ${folder.name} not found`); - const subs = folder.getDirectories(); - // load files in this folder - let items: DefaultTheme.SidebarItem[] = folder.getFiles().length - ? filesToSidebarItems(folder.getFiles(), `${base}/${folder.name}`) - : []; - for (const index in subs) { - if (Object.prototype.hasOwnProperty.call(subs, index)) { - const sub = subs[index]; - const currentSidebarItem: DefaultTheme.SidebarItem = { - collapsed: false, - text: sub.name.replace(/^\d+\.\s*/, ''), // remove leading index - items: folderToSidebarItems(sub, `${base}/${folder.name}`), - }; - items.push(currentSidebarItem); - } - } - return items; +export async function getRegisteredMarkdownTheme( + theme: keyof typeof themes +): Promise { + let isThemeRegistered = (await shikiji.getSingletonHighlighter()) + .getLoadedThemes() + .find(x => x === theme); + if (!isThemeRegistered) { + const myTheme = JSON.parse( + fs.readFileSync(path.join(projectRoot().fullName, `public/${theme}.json`), 'utf8') + ); + (await shikiji.getSingletonHighlighter()).loadTheme(myTheme); + } + return (await shikiji.getSingletonHighlighter()).getTheme(theme); }