@ox-content/vite-plugin 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6725,39 +6725,40 @@ var import_dist = /* @__PURE__ */ require_chunk.__toESM(require_dist(), 1);
6725
6725
  /**
6726
6726
  * Syntax highlighting with Shiki via rehype.
6727
6727
  */
6728
+ const BUILTIN_LANGS = [
6729
+ "javascript",
6730
+ "typescript",
6731
+ "jsx",
6732
+ "tsx",
6733
+ "vue",
6734
+ "svelte",
6735
+ "html",
6736
+ "css",
6737
+ "scss",
6738
+ "json",
6739
+ "yaml",
6740
+ "markdown",
6741
+ "bash",
6742
+ "shell",
6743
+ "rust",
6744
+ "python",
6745
+ "go",
6746
+ "java",
6747
+ "c",
6748
+ "cpp",
6749
+ "sql",
6750
+ "graphql",
6751
+ "diff",
6752
+ "toml"
6753
+ ];
6728
6754
  let highlighterPromise = null;
6729
6755
  /**
6730
6756
  * Get or create the Shiki highlighter.
6731
6757
  */
6732
- async function getHighlighter(theme) {
6758
+ async function getHighlighter(theme, customLangs = []) {
6733
6759
  if (!highlighterPromise) highlighterPromise = (0, shiki.createHighlighter)({
6734
6760
  themes: [theme],
6735
- langs: [
6736
- "javascript",
6737
- "typescript",
6738
- "jsx",
6739
- "tsx",
6740
- "vue",
6741
- "svelte",
6742
- "html",
6743
- "css",
6744
- "scss",
6745
- "json",
6746
- "yaml",
6747
- "markdown",
6748
- "bash",
6749
- "shell",
6750
- "rust",
6751
- "python",
6752
- "go",
6753
- "java",
6754
- "c",
6755
- "cpp",
6756
- "sql",
6757
- "graphql",
6758
- "diff",
6759
- "toml"
6760
- ]
6761
+ langs: [...BUILTIN_LANGS, ...customLangs]
6761
6762
  });
6762
6763
  return highlighterPromise;
6763
6764
  }
@@ -6765,9 +6766,9 @@ async function getHighlighter(theme) {
6765
6766
  * Rehype plugin for syntax highlighting with Shiki.
6766
6767
  */
6767
6768
  function rehypeShikiHighlight(options) {
6768
- const { theme } = options;
6769
+ const { theme, langs } = options;
6769
6770
  return async (tree) => {
6770
- const highlighter = await getHighlighter(theme);
6771
+ const highlighter = await getHighlighter(theme, langs);
6771
6772
  const visit = async (node) => {
6772
6773
  if ("children" in node) for (let i = 0; i < node.children.length; i++) {
6773
6774
  const child = node.children[i];
@@ -6810,8 +6811,11 @@ function getTextContent(node) {
6810
6811
  /**
6811
6812
  * Apply syntax highlighting to HTML using Shiki.
6812
6813
  */
6813
- async function highlightCode(html, theme = "github-dark") {
6814
- const result = await (0, unified.unified)().use(rehype_parse.default, { fragment: true }).use(rehypeShikiHighlight, { theme }).use(rehype_stringify.default).process(html);
6814
+ async function highlightCode(html, theme = "github-dark", langs = []) {
6815
+ const result = await (0, unified.unified)().use(rehype_parse.default, { fragment: true }).use(rehypeShikiHighlight, {
6816
+ theme,
6817
+ langs
6818
+ }).use(rehype_stringify.default).process(html);
6815
6819
  return String(result);
6816
6820
  }
6817
6821
 
@@ -6989,7 +6993,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
6989
6993
  if (options.mermaid) html = await require_mermaid.transformMermaidStatic(html);
6990
6994
  const { html: protectedHtml, svgs } = protectMermaidSvgs(html);
6991
6995
  html = protectedHtml;
6992
- if (options.highlight) html = await highlightCode(html, options.highlightTheme);
6996
+ if (options.highlight) html = await highlightCode(html, options.highlightTheme, options.highlightLangs);
6993
6997
  html = restoreMermaidSvgs(html, svgs);
6994
6998
  return {
6995
6999
  code: generateModuleCode(html, frontmatter, toc, filePath, options),
@@ -10960,6 +10964,251 @@ function createOgViewerPlugin(options) {
10960
10964
  };
10961
10965
  }
10962
10966
 
10967
+ //#endregion
10968
+ //#region src/i18n.ts
10969
+ /**
10970
+ * i18n plugin for Ox Content.
10971
+ *
10972
+ * Provides:
10973
+ * - Dictionary loading and validation at build time
10974
+ * - Virtual module for i18n config
10975
+ * - Build-time i18n checking
10976
+ * - Locale-aware routing middleware for dev server
10977
+ */
10978
+ /**
10979
+ * Resolves i18n options with defaults.
10980
+ */
10981
+ function resolveI18nOptions(options) {
10982
+ if (options === false) return false;
10983
+ if (!options || !options.enabled) return false;
10984
+ const defaultLocale = options.defaultLocale ?? "en";
10985
+ const locales = options.locales ?? [{
10986
+ code: defaultLocale,
10987
+ name: defaultLocale
10988
+ }];
10989
+ if (!locales.some((l) => l.code === defaultLocale)) locales.unshift({
10990
+ code: defaultLocale,
10991
+ name: defaultLocale
10992
+ });
10993
+ return {
10994
+ enabled: true,
10995
+ dir: options.dir ?? "content/i18n",
10996
+ defaultLocale,
10997
+ locales,
10998
+ hideDefaultLocale: options.hideDefaultLocale ?? true,
10999
+ check: options.check ?? true,
11000
+ functionNames: options.functionNames ?? ["t", "$t"]
11001
+ };
11002
+ }
11003
+ /**
11004
+ * Creates the i18n sub-plugin for the Vite plugin array.
11005
+ */
11006
+ function createI18nPlugin(resolvedOptions) {
11007
+ const i18nOptions = resolvedOptions.i18n;
11008
+ let root = process.cwd();
11009
+ return {
11010
+ name: "ox-content:i18n",
11011
+ configResolved(config) {
11012
+ root = config.root;
11013
+ },
11014
+ resolveId(id) {
11015
+ if (id === "virtual:ox-content/i18n") return "\0virtual:ox-content/i18n";
11016
+ return null;
11017
+ },
11018
+ load(id) {
11019
+ if (id === "\0virtual:ox-content/i18n") {
11020
+ if (!i18nOptions) return `export const i18n = { enabled: false }; export default i18n;`;
11021
+ return generateI18nModule(i18nOptions, root);
11022
+ }
11023
+ return null;
11024
+ },
11025
+ async buildStart() {
11026
+ if (!i18nOptions || !i18nOptions.check) return;
11027
+ const dictDir = path$1.resolve(root, i18nOptions.dir);
11028
+ if (!fs.existsSync(dictDir)) {
11029
+ console.warn(`[ox-content:i18n] Dictionary directory not found: ${dictDir}`);
11030
+ return;
11031
+ }
11032
+ try {
11033
+ const { loadDictionaries, checkI18n, extractTranslationKeys } = await import("@ox-content/napi");
11034
+ const loadResult = loadDictionaries(dictDir);
11035
+ if (loadResult.errors.length > 0) {
11036
+ for (const error of loadResult.errors) console.warn(`[ox-content:i18n] ${error}`);
11037
+ return;
11038
+ }
11039
+ console.log(`[ox-content:i18n] Loaded ${loadResult.localeCount} locales: ${loadResult.locales.join(", ")}`);
11040
+ const checkResult = checkI18n(dictDir, collectKeysFromSource(root, extractTranslationKeys, i18nOptions));
11041
+ if (checkResult.errorCount > 0 || checkResult.warningCount > 0) {
11042
+ for (const diag of checkResult.diagnostics) if (diag.severity === "error") console.error(`[ox-content:i18n] ${diag.message}`);
11043
+ else if (diag.severity === "warning") console.warn(`[ox-content:i18n] ${diag.message}`);
11044
+ }
11045
+ } catch {}
11046
+ },
11047
+ configureServer(server) {
11048
+ if (!i18nOptions) return;
11049
+ const dictDir = path$1.resolve(root, i18nOptions.dir);
11050
+ if (fs.existsSync(dictDir)) {
11051
+ server.watcher.add(dictDir);
11052
+ server.watcher.on("change", (filePath) => {
11053
+ if (!filePath.startsWith(dictDir)) return;
11054
+ if (!/\.(json|yaml|yml)$/.test(filePath)) return;
11055
+ const mod = server.moduleGraph.getModuleById("\0virtual:ox-content/i18n");
11056
+ if (mod) server.moduleGraph.invalidateModule(mod);
11057
+ server.ws.send({ type: "full-reload" });
11058
+ });
11059
+ }
11060
+ server.middlewares.use((req, _res, next) => {
11061
+ if (!req.url) return next();
11062
+ const localeMatch = req.url.match(/^\/([a-z]{2}(?:-[a-zA-Z]+)?)(\/|$)/);
11063
+ if (localeMatch) {
11064
+ const localeCode = localeMatch[1];
11065
+ if (i18nOptions.locales.some((l) => l.code === localeCode)) req.__oxLocale = localeCode;
11066
+ } else if (i18nOptions.hideDefaultLocale) req.__oxLocale = i18nOptions.defaultLocale;
11067
+ next();
11068
+ });
11069
+ }
11070
+ };
11071
+ }
11072
+ /**
11073
+ * Generates the virtual module for i18n configuration.
11074
+ */
11075
+ function generateI18nModule(options, root) {
11076
+ const dictDir = path$1.resolve(root, options.dir);
11077
+ const localesJson = JSON.stringify(options.locales);
11078
+ const defaultLocale = JSON.stringify(options.defaultLocale);
11079
+ let dictionariesCode = "{}";
11080
+ try {
11081
+ const napi = require("@ox-content/napi");
11082
+ if (napi.loadDictionariesFlat) {
11083
+ const dictData = napi.loadDictionariesFlat(dictDir);
11084
+ dictionariesCode = JSON.stringify(dictData);
11085
+ } else dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
11086
+ } catch {
11087
+ try {
11088
+ dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
11089
+ } catch {}
11090
+ }
11091
+ return `
11092
+ export const i18nConfig = {
11093
+ enabled: true,
11094
+ defaultLocale: ${defaultLocale},
11095
+ locales: ${localesJson},
11096
+ hideDefaultLocale: ${JSON.stringify(options.hideDefaultLocale)},
11097
+ };
11098
+
11099
+ export const dictionaries = ${dictionariesCode};
11100
+
11101
+ export function t(key, params, locale) {
11102
+ const dict = dictionaries[locale || i18nConfig.defaultLocale] || {};
11103
+ let message = dict[key];
11104
+ if (!message) {
11105
+ const fallback = dictionaries[i18nConfig.defaultLocale] || {};
11106
+ message = fallback[key] || key;
11107
+ }
11108
+ if (params) {
11109
+ for (const [k, v] of Object.entries(params)) {
11110
+ message = message.replace(new RegExp('\\\\{\\\\$' + k + '\\\\}', 'g'), String(v));
11111
+ }
11112
+ }
11113
+ return message;
11114
+ }
11115
+
11116
+ export function getLocaleFromPath(pathname) {
11117
+ const match = pathname.match(/^\\/([a-z]{2}(?:-[a-zA-Z]+)?)(\\//|$)/);
11118
+ if (match) {
11119
+ const code = match[1];
11120
+ if (i18nConfig.locales.some(l => l.code === code)) {
11121
+ return code;
11122
+ }
11123
+ }
11124
+ return i18nConfig.defaultLocale;
11125
+ }
11126
+
11127
+ export function localePath(pathname, locale) {
11128
+ const current = getLocaleFromPath(pathname);
11129
+ let clean = pathname;
11130
+ if (current !== i18nConfig.defaultLocale || !i18nConfig.hideDefaultLocale) {
11131
+ clean = pathname.replace(new RegExp('^/' + current + '(/|$)'), '/');
11132
+ }
11133
+ if (locale === i18nConfig.defaultLocale && i18nConfig.hideDefaultLocale) {
11134
+ return clean || '/';
11135
+ }
11136
+ return '/' + locale + (clean.startsWith('/') ? clean : '/' + clean);
11137
+ }
11138
+
11139
+ export default { i18nConfig, dictionaries, t, getLocaleFromPath, localePath };
11140
+ `;
11141
+ }
11142
+ /**
11143
+ * Flattens a nested object into dot-separated keys.
11144
+ */
11145
+ function flattenObject(obj, prefix, result) {
11146
+ for (const [key, value] of Object.entries(obj)) {
11147
+ const fullKey = `${prefix}.${key}`;
11148
+ if (typeof value === "string") result[fullKey] = value;
11149
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenObject(value, fullKey, result);
11150
+ else result[fullKey] = String(value);
11151
+ }
11152
+ }
11153
+ /**
11154
+ * Fallback dictionary loading using TS-based JSON file reading.
11155
+ */
11156
+ function loadDictionariesFallback(options, dictDir) {
11157
+ const dictData = {};
11158
+ for (const locale of options.locales) {
11159
+ const localeDir = path$1.join(dictDir, locale.code);
11160
+ if (!fs.existsSync(localeDir)) continue;
11161
+ const files = fs.readdirSync(localeDir);
11162
+ const localeDict = {};
11163
+ for (const file of files) {
11164
+ if (!file.endsWith(".json")) continue;
11165
+ const filePath = path$1.join(localeDir, file);
11166
+ const content = fs.readFileSync(filePath, "utf-8");
11167
+ const namespace = path$1.basename(file, ".json");
11168
+ try {
11169
+ flattenObject(JSON.parse(content), namespace, localeDict);
11170
+ } catch {}
11171
+ }
11172
+ dictData[locale.code] = localeDict;
11173
+ }
11174
+ return dictData;
11175
+ }
11176
+ /**
11177
+ * Collects translation keys from source files using NAPI extractTranslationKeys.
11178
+ */
11179
+ function collectKeysFromSource(root, extractTranslationKeys, options) {
11180
+ const srcDir = path$1.resolve(root, "src");
11181
+ const keys = /* @__PURE__ */ new Set();
11182
+ if (fs.existsSync(srcDir)) walkDir(srcDir, /\.(ts|tsx|js|jsx)$/, (filePath) => {
11183
+ const usages = extractTranslationKeys(fs.readFileSync(filePath, "utf-8"), filePath, options.functionNames);
11184
+ for (const usage of usages) keys.add(usage.key);
11185
+ });
11186
+ const contentDir = path$1.resolve(root, "content");
11187
+ if (fs.existsSync(contentDir)) {
11188
+ const tPattern = /\{\{t\(['"]([^'"]+)['"]\)\}\}/g;
11189
+ walkDir(contentDir, /\.(md|mdx)$/, (filePath) => {
11190
+ const content = fs.readFileSync(filePath, "utf-8");
11191
+ let match;
11192
+ while ((match = tPattern.exec(content)) !== null) keys.add(match[1]);
11193
+ tPattern.lastIndex = 0;
11194
+ });
11195
+ }
11196
+ return Array.from(keys);
11197
+ }
11198
+ /**
11199
+ * Recursively walks a directory, calling the callback for files matching the pattern.
11200
+ */
11201
+ function walkDir(dir, pattern, callback) {
11202
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
11203
+ for (const entry of entries) {
11204
+ const fullPath = path$1.join(dir, entry.name);
11205
+ if (entry.isDirectory()) {
11206
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
11207
+ walkDir(fullPath, pattern, callback);
11208
+ } else if (pattern.test(entry.name)) callback(fullPath);
11209
+ }
11210
+ }
11211
+
10963
11212
  //#endregion
10964
11213
  //#region src/jsx-runtime.ts
10965
11214
  /**
@@ -11686,6 +11935,7 @@ function oxContent(options = {}) {
11686
11935
  }
11687
11936
  }
11688
11937
  ];
11938
+ if (resolvedOptions.i18n) plugins.push(createI18nPlugin(resolvedOptions));
11689
11939
  if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
11690
11940
  return plugins;
11691
11941
  }
@@ -11705,6 +11955,7 @@ function resolveOptions(options) {
11705
11955
  strikethrough: options.strikethrough ?? true,
11706
11956
  highlight: options.highlight ?? false,
11707
11957
  highlightTheme: options.highlightTheme ?? "github-dark",
11958
+ highlightLangs: options.highlightLangs ?? [],
11708
11959
  mermaid: options.mermaid ?? false,
11709
11960
  frontmatter: options.frontmatter ?? true,
11710
11961
  toc: options.toc ?? true,
@@ -11714,7 +11965,8 @@ function resolveOptions(options) {
11714
11965
  transformers: options.transformers ?? [],
11715
11966
  docs: resolveDocsOptions(options.docs),
11716
11967
  search: resolveSearchOptions(options.search),
11717
- ogViewer: options.ogViewer ?? true
11968
+ ogViewer: options.ogViewer ?? true,
11969
+ i18n: resolveI18nOptions(options.i18n)
11718
11970
  };
11719
11971
  }
11720
11972
  /**
@@ -11744,6 +11996,7 @@ exports.buildSsg = buildSsg;
11744
11996
  exports.clearRenderContext = clearRenderContext;
11745
11997
  exports.collectGitHubRepos = require_github.collectGitHubRepos;
11746
11998
  exports.collectOgpUrls = require_ogp.collectOgpUrls;
11999
+ exports.createI18nPlugin = createI18nPlugin;
11747
12000
  exports.createMarkdownEnvironment = createMarkdownEnvironment;
11748
12001
  exports.createTheme = createTheme;
11749
12002
  exports.defaultTheme = defaultTheme;
@@ -11774,6 +12027,7 @@ exports.renderAllPages = renderAllPages;
11774
12027
  exports.renderPage = renderPage;
11775
12028
  exports.renderToString = renderToString;
11776
12029
  exports.resolveDocsOptions = resolveDocsOptions;
12030
+ exports.resolveI18nOptions = resolveI18nOptions;
11777
12031
  exports.resolveOgImageOptions = resolveOgImageOptions;
11778
12032
  exports.resolveSearchOptions = resolveSearchOptions;
11779
12033
  exports.resolveSsgOptions = resolveSsgOptions;