import fs from "node:fs"; import { IncomingMessage, ServerResponse } from "node:http"; import https from "node:https"; import path from "node:path"; import { Connect, Plugin, ViteDevServer } from "vite"; import { defineConfig, MarkdownOptions } from "vitepress"; const host = process.env.TAURI_DEV_HOST; // https://vitepress.dev/reference/site-config export default defineConfig({ title: "ko-grammar", description: "Korean grammar edited", lang: "ko-KR", themeConfig: { socialLinks: [ { icon: "github", link: "https://git.polv.cc/polv/ko-grammar" }, ], outline: { level: "deep", }, }, markdown: { config(md) { const mdInline: MarkdownOptions["config"] = (md) => { const className = "md"; md.core.ruler.after("inline", "inline_class_md", (state: any) => { const tokens = state.tokens; const tagPattern = new RegExp( `class=(?:\"|')([^\"']*\\b${escapeReg(className)}\\b[^\"']*)(?:\"|')`, "i", ); for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (!/^(html_inline|html_block)$/.test(t.type)) continue; const html = t.content; // match tags with class including the target class, capture inner content (non-greedy) const pattern = new RegExp( `<([a-zA-Z0-9-]+)(\\b[^>]*)?>([\\s\\S]*?)<\\/\\1>`, "gi", ); let lastIndex = 0; let out = ""; let m; while ((m = pattern.exec(html)) !== null) { const [full, tagName, attrs = "", inner] = m; const idx = m.index; if (idx > lastIndex) out += html.slice(lastIndex, idx); if (tagPattern.test(attrs)) { // render inner as inline markdown out += `<${tagName}${attrs}>${md.renderInline(inner)}`; } else { out += full; } lastIndex = idx + full.length; } if (lastIndex === 0) continue; if (lastIndex < html.length) out += html.slice(lastIndex); t.content = out; } }); function escapeReg(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } }; md.use(mdInline); }, }, vite: { plugins: [audioProxyPlugin() as any], envDir: process.cwd(), ssr: { noExternal: [], external: ["pdfjs-dist"], // don't bundle it for SSR — leave it browser-only }, clearScreen: false, server: { /** * Allow Windows Firewall via Admin PowerShell with * * ``` * New-NetFirewallRule -DisplayName "Vitepress Server" -Direction Inbound -Protocol TCP -LocalPort 5173 -Action Allow * ``` */ port: 5173, strictPort: true, host: host || false, watch: { // tell vite to ignore watching `src-tauri` ignored: ["**/src-tauri/**"], }, hmr: host ? { protocol: "ws", host, port: 5174, } : undefined, }, // Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`. envPrefix: ["VITE_", "TAURI_ENV_*"], build: { // Tauri uses Chromium on Windows and WebKit on macOS and Linux target: process.env.TAURI_ENV_PLATFORM == "windows" ? "chrome105" : "safari13", // don't minify for debug builds minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, // produce sourcemaps for debug builds sourcemap: !!process.env.TAURI_ENV_DEBUG, }, }, }); function audioProxyPlugin(): Plugin { return { name: "audio-proxy", configureServer(server: ViteDevServer) { // Vite dev server middlewares are connect-style: (req, res, next) server.middlewares.use((req, res, next) => { const reqUrl = req.url; if (!reqUrl || !reqUrl.startsWith("/uploads/")) return next(); if (!reqUrl.endsWith(".mp3")) return next(); proxyAudio(req, res, next); }); }, }; } function proxyAudio( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction, ) { const url = new URL( `https://www.howtostudykorean.com/wp-content/uploads${req.url}`, ); // Forward common headers (important for Range streaming) const headers = { ...req.headers, host: url.host, range: req.headers["range"], }; const segs = req.url!.slice(1).split("/"); const filename = segs.pop()!; const saveroot = "docs/public"; const savetemp = path.join(saveroot, ...segs, filename + ".temp"); const saveto = path.join(saveroot, ...segs, filename); if (fs.existsSync(saveto)) { next(); return; } fs.mkdirSync(path.join(saveroot, ...segs), { recursive: true }); const fileStream = fs.createWriteStream(savetemp); const upstreamReq = https.request( { protocol: url.protocol, hostname: url.hostname, port: url.port || undefined, path: url.pathname + url.search, method: req.method, headers, }, (upstreamRes) => { // Pass status (200 or 206 for Range) res.writeHead(upstreamRes.statusCode || 200, upstreamRes.headers); // Stream through upstreamRes.pipe(res); upstreamRes.pipe(fileStream); }, ); fileStream.on("finish", () => { fs.renameSync(savetemp, saveto); console.log("saved to", saveto); }); fileStream.on("error", (err) => { console.error("write failed:", err); }); upstreamReq.on("error", (err) => { res.statusCode = 502; res.end(`Bad Gateway: ${err.message}`); }); req.pipe(upstreamReq); }