mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
306 lines
9.4 KiB
TypeScript
306 lines
9.4 KiB
TypeScript
import { existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
|
|
|
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
|
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
|
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
|
const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`;
|
|
const PLAUSIBLE_INIT_SCRIPT = [
|
|
'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};',
|
|
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
|
].join('\n');
|
|
|
|
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
|
|
|
type VersionManifest = {
|
|
latestStable: string;
|
|
channels: Array<{ label: string; path: string }>;
|
|
versions: Array<{ version: string; path: string }>;
|
|
};
|
|
|
|
const base = normalizeBase(process.env.SUBMINER_DOCS_BASE ?? '/');
|
|
const outDir = process.env.SUBMINER_DOCS_OUT_DIR;
|
|
const channel = normalizeChannel(process.env.SUBMINER_DOCS_CHANNEL);
|
|
const docsVersion = process.env.SUBMINER_DOCS_VERSION;
|
|
const latestStable = process.env.SUBMINER_DOCS_LATEST_STABLE ?? 'v0.14.0';
|
|
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
|
|
|
|
function normalizeBase(value: string): string {
|
|
if (!value || value === '/') return '/';
|
|
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
|
|
}
|
|
|
|
function normalizeChannel(value: string | undefined): DocsChannel {
|
|
if (value === 'main' || value === 'stable-archive') return value;
|
|
return 'stable-root';
|
|
}
|
|
|
|
function parseVersionManifest(value: string | undefined): VersionManifest {
|
|
if (!value) {
|
|
return {
|
|
latestStable,
|
|
channels: [
|
|
{ label: 'Latest stable', path: '/' },
|
|
{ label: 'main', path: '/main/' },
|
|
],
|
|
versions: [{ version: latestStable, path: `/v/${latestStable.replace(/^v/, '')}/` }],
|
|
};
|
|
}
|
|
|
|
return JSON.parse(value) as VersionManifest;
|
|
}
|
|
|
|
function withDocsBase(path: string): string {
|
|
if (/^[a-z]+:\/\//i.test(path)) return path;
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
if (base === '/') return normalizedPath;
|
|
return `${base.replace(/\/$/, '')}${normalizedPath}`;
|
|
}
|
|
|
|
function pageToRoute(page: string): string | null {
|
|
if (page === '404.md') return null;
|
|
|
|
const route = page
|
|
.replace(/(^|\/)index\.md$/, '')
|
|
.replace(/\.md$/, '')
|
|
.replace(/\/$/, '');
|
|
return route ? `/${route}` : '/';
|
|
}
|
|
|
|
function pageToCanonicalHref(page: string): string | null {
|
|
const route = pageToRoute(page);
|
|
if (!route) return null;
|
|
|
|
if (channel === 'main') {
|
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
|
}
|
|
|
|
if (channel === 'stable-archive' && docsVersion !== latestStable) {
|
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
|
}
|
|
|
|
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
|
|
}
|
|
|
|
function canonicalRouteWithBase(route: string): string {
|
|
const routeWithBase = withDocsBase(route);
|
|
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
|
|
}
|
|
|
|
function transformPageHead({ page }: TransformContext): HeadConfig[] {
|
|
const href = pageToCanonicalHref(page);
|
|
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
|
|
|
|
if (channel === 'main') {
|
|
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
|
}
|
|
|
|
return head;
|
|
}
|
|
|
|
function linkToPagePath(link: string): string | null {
|
|
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
|
|
return null;
|
|
}
|
|
|
|
const withoutHash = link.split('#')[0] ?? '/';
|
|
const withoutQuery = withoutHash.split('?')[0] ?? '/';
|
|
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
|
|
return route ? `${route}.md` : 'index.md';
|
|
}
|
|
|
|
function hasPageForLink(link: string): boolean {
|
|
const pagePath = linkToPagePath(link);
|
|
if (!pagePath) return true;
|
|
return existsSync(join(process.cwd(), pagePath));
|
|
}
|
|
|
|
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
|
|
return items
|
|
.map((item) => {
|
|
if ('items' in item && item.items) {
|
|
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
|
|
}
|
|
if ('link' in item && item.link && !hasPageForLink(item.link)) {
|
|
return null;
|
|
}
|
|
return item;
|
|
})
|
|
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
|
|
}
|
|
|
|
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
|
|
return items
|
|
.map((item) => {
|
|
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
|
|
if (item.link && !hasPageForLink(item.link)) return null;
|
|
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
|
|
return { ...item, items: filteredChildren };
|
|
})
|
|
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
|
|
}
|
|
|
|
const versionItems = [
|
|
{
|
|
text: `Latest stable (${versionManifest.latestStable})`,
|
|
link: '/',
|
|
},
|
|
...versionManifest.channels
|
|
.filter((entry) => entry.label !== 'Latest stable')
|
|
.map((entry) => ({ text: entry.label, link: entry.path })),
|
|
...versionManifest.versions.map((entry) => ({
|
|
text: entry.version,
|
|
link: entry.path,
|
|
})),
|
|
];
|
|
|
|
const nav: DefaultTheme.NavItem[] = [
|
|
{ text: 'Home', link: '/' },
|
|
{ text: 'Get Started', link: '/installation' },
|
|
{ text: 'Mining', link: '/mining-workflow' },
|
|
{ text: 'Configuration', link: '/configuration' },
|
|
{ text: 'Changelog', link: '/changelog' },
|
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
{ text: docsVersion ?? (channel === 'main' ? 'main' : latestStable), items: versionItems },
|
|
];
|
|
|
|
const sidebar: DefaultTheme.SidebarItem[] = [
|
|
{
|
|
text: 'Getting Started',
|
|
items: [
|
|
{ text: 'Overview', link: '/' },
|
|
{ text: 'Installation', link: '/installation' },
|
|
{ text: 'Usage', link: '/usage' },
|
|
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
|
{ text: 'Launcher Script', link: '/launcher-script' },
|
|
],
|
|
},
|
|
{
|
|
text: 'Reference',
|
|
items: [
|
|
{ text: 'Configuration', link: '/configuration' },
|
|
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
|
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
|
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
|
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
|
],
|
|
},
|
|
{
|
|
text: 'Integrations',
|
|
items: [
|
|
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
|
{ text: 'Anki', link: '/anki-integration' },
|
|
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
|
{ text: 'YouTube', link: '/youtube-integration' },
|
|
{ text: 'Jimaku', link: '/jimaku-integration' },
|
|
{ text: 'AniList', link: '/anilist-integration' },
|
|
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
|
],
|
|
},
|
|
{
|
|
text: 'Development',
|
|
items: [
|
|
{ text: 'Building & Testing', link: '/development' },
|
|
{ text: 'Architecture', link: '/architecture' },
|
|
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
|
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
|
{ text: 'Changelog', link: '/changelog' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const config: UserConfig = {
|
|
title: 'SubMiner Docs',
|
|
description:
|
|
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
|
base,
|
|
...(outDir ? { outDir } : {}),
|
|
head: [
|
|
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
|
[
|
|
'script',
|
|
{
|
|
async: '',
|
|
src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`,
|
|
},
|
|
],
|
|
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
|
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
|
|
[
|
|
'link',
|
|
{
|
|
rel: 'icon',
|
|
type: 'image/png',
|
|
href: withDocsBase('/favicon-32x32.png'),
|
|
sizes: '32x32',
|
|
},
|
|
],
|
|
[
|
|
'link',
|
|
{
|
|
rel: 'icon',
|
|
type: 'image/png',
|
|
href: withDocsBase('/favicon-16x16.png'),
|
|
sizes: '16x16',
|
|
},
|
|
],
|
|
[
|
|
'link',
|
|
{
|
|
rel: 'apple-touch-icon',
|
|
href: withDocsBase('/apple-touch-icon.png'),
|
|
sizes: '180x180',
|
|
},
|
|
],
|
|
],
|
|
appearance: 'dark',
|
|
cleanUrls: true,
|
|
metaChunk: true,
|
|
sitemap: {
|
|
hostname: DOCS_HOSTNAME,
|
|
transformItems(items) {
|
|
return items.filter(
|
|
(item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`,
|
|
);
|
|
},
|
|
},
|
|
transformHead: transformPageHead,
|
|
lastUpdated: true,
|
|
srcExclude: ['subagents/**'],
|
|
markdown: {
|
|
theme: {
|
|
light: 'catppuccin-latte',
|
|
dark: 'catppuccin-macchiato',
|
|
},
|
|
},
|
|
themeConfig: {
|
|
logo: {
|
|
light: withDocsBase('/assets/SubMiner.png'),
|
|
dark: withDocsBase('/assets/SubMiner.png'),
|
|
},
|
|
siteTitle: 'SubMiner Docs',
|
|
nav: filterNav(nav),
|
|
sidebar: filterSidebar(sidebar),
|
|
search: {
|
|
provider: 'local',
|
|
},
|
|
footer: {
|
|
message: 'Released under the GPL-3.0 License.',
|
|
copyright: 'Copyright © 2026-present sudacode',
|
|
},
|
|
editLink: {
|
|
pattern: 'https://github.com/ksyasuda/SubMiner/edit/main/docs-site/:path',
|
|
text: 'Edit this page on GitHub',
|
|
},
|
|
outline: { level: [2, 3], label: 'On this page' },
|
|
externalLinkIcon: true,
|
|
docFooter: { prev: 'Previous', next: 'Next' },
|
|
returnToTopLabel: 'Back to top',
|
|
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
|
},
|
|
};
|
|
|
|
export default config;
|