hexo-theme-gnix 8.0.0 → 10.0.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.
Files changed (66) hide show
  1. package/README.md +4 -2
  2. package/include/hexo/feed.js +6 -5
  3. package/include/hexo/filter.js +25 -1
  4. package/include/hexo/generator/archive.js +116 -0
  5. package/include/hexo/generator/home.js +64 -0
  6. package/include/hexo/generator/index.js +82 -0
  7. package/include/hexo/generator/md_generator.js +87 -0
  8. package/include/hexo/generator/page.js +55 -0
  9. package/include/hexo/generator/tag.js +84 -0
  10. package/include/hexo/helper.js +38 -0
  11. package/include/hexo/i18n.js +183 -0
  12. package/include/util/article_font.js +132 -0
  13. package/include/util/i18n.js +280 -0
  14. package/include/util/theme.js +84 -0
  15. package/languages/en.yml +28 -0
  16. package/languages/zh-CN.yml +28 -0
  17. package/layout/archive.jsx +131 -127
  18. package/layout/common/article.jsx +316 -34
  19. package/layout/common/article_info.jsx +339 -0
  20. package/layout/common/article_media.jsx +11 -4
  21. package/layout/common/comment.jsx +15 -7
  22. package/layout/common/footer.jsx +6 -5
  23. package/layout/common/head.jsx +122 -33
  24. package/layout/common/navbar.jsx +195 -65
  25. package/layout/common/theme_selector.jsx +16 -14
  26. package/layout/layout.jsx +43 -5
  27. package/layout/misc/open_graph.jsx +162 -66
  28. package/layout/misc/paginator.jsx +2 -8
  29. package/layout/plugin/cookie_consent.jsx +252 -53
  30. package/layout/plugin/swup.jsx +1 -1
  31. package/layout/search/insight.jsx +1 -1
  32. package/layout/tag.jsx +3 -2
  33. package/layout/tags.jsx +81 -73
  34. package/package.json +5 -5
  35. package/scripts/index.js +1 -0
  36. package/source/css/archive.css +225 -180
  37. package/source/css/default.css +1223 -126
  38. package/source/css/responsive.css +426 -0
  39. package/source/css/shiki/shiki.css +12 -2081
  40. package/source/css/tags.css +183 -0
  41. package/source/css/twikoo.css +1053 -1049
  42. package/source/img/favicon.svg +1 -6
  43. package/source/img/og_image.webp +0 -0
  44. package/source/js/article-font-utils.js +99 -0
  45. package/source/js/busuanzi.js +91 -24
  46. package/source/js/components/chat.js +169 -50
  47. package/source/js/components/image-carousel.js +152 -108
  48. package/source/js/components/sidenote.js +210 -0
  49. package/source/js/components/text-image-section.js +78 -90
  50. package/source/js/components/theme-stacked.js +65 -33
  51. package/source/js/components/tree.js +30 -16
  52. package/source/js/decrypt.js +7 -2
  53. package/source/js/main.js +428 -5
  54. package/source/js/swup.js +39 -0
  55. package/source/js/theme-selector.js +26 -16
  56. package/include/hexo/generator.js +0 -53
  57. package/layout/misc/article_licensing.jsx +0 -99
  58. package/source/css/responsive/desktop.css +0 -36
  59. package/source/css/responsive/mobile.css +0 -38
  60. package/source/css/responsive/tablet.css +0 -43
  61. package/source/css/responsive/touch.css +0 -155
  62. package/source/img/logo.svg +0 -9
  63. package/source/js/archive-breadcrumb.js +0 -132
  64. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.css +0 -6
  65. package/source/js/host/cookieconsent/3.1.1/build/cookieconsent.min.js +0 -1
  66. package/source/js/swup.bundle.js +0 -1
@@ -0,0 +1,183 @@
1
+ const { extname } = require("node:path");
2
+ const createPostProcessor = require("hexo/dist/plugins/processor/post");
3
+ const { isHiddenFile, isMatch, isTmpFile } = require("hexo/dist/plugins/processor/common");
4
+ const {
5
+ getI18nKey,
6
+ getLanguageBasePath,
7
+ getLanguage,
8
+ getLanguageKeys,
9
+ getPageLanguageKey,
10
+ inferI18nKeyFromSource,
11
+ isI18nEnabled,
12
+ localizePath,
13
+ trimSlashes,
14
+ } = require("../util/i18n");
15
+
16
+ function getConfig(hexo, locals = {}) {
17
+ return Object.assign({}, hexo.config, hexo.config.theme_config, hexo.theme.config, locals.theme);
18
+ }
19
+
20
+ function getLocalizedPostParams(hexo, sourcePath) {
21
+ const config = getConfig(hexo);
22
+ if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
23
+
24
+ const match = sourcePath.replace(/\\/g, "/").match(/^([^/]+)\/(_posts|_drafts)\/(.+)$/);
25
+ if (!match) return null;
26
+
27
+ const langKey = match[1];
28
+ if (!getLanguageKeys(config).includes(langKey)) return null;
29
+
30
+ const postPath = match[3];
31
+ if (isHiddenFile(postPath)) return null;
32
+
33
+ let renderable = hexo.render.isRenderable(sourcePath) && !isMatch(sourcePath, hexo.config.skip_render);
34
+ if (renderable && hexo.config.post_asset_folder) {
35
+ renderable = extname(hexo.config.new_post_name) === extname(sourcePath);
36
+ }
37
+
38
+ return {
39
+ i18n_lang: langKey,
40
+ path: postPath,
41
+ published: match[2] === "_posts",
42
+ renderable,
43
+ };
44
+ }
45
+
46
+ function getLocalizedPageParams(hexo, sourcePath) {
47
+ const config = getConfig(hexo);
48
+ if (!isI18nEnabled(config) || isTmpFile(sourcePath)) return null;
49
+
50
+ const normalized = sourcePath.replace(/\\/g, "/");
51
+ const match = normalized.match(/^([^/]+)\/(.+)$/);
52
+ if (!match) return null;
53
+
54
+ const langKey = match[1];
55
+ if (!getLanguageKeys(config).includes(langKey)) return null;
56
+
57
+ const pagePath = match[2];
58
+ if (pagePath.startsWith("_posts/") || pagePath.startsWith("_drafts/") || isHiddenFile(pagePath)) return null;
59
+
60
+ const renderable = hexo.render.isRenderable(sourcePath) && !isMatch(sourcePath, hexo.config.skip_render);
61
+ if (!renderable) return null;
62
+
63
+ return {
64
+ i18n_lang: langKey,
65
+ };
66
+ }
67
+
68
+ function buildPostI18nUpdate(post, langKey, config) {
69
+ const language = getLanguage(config, langKey);
70
+ const update = {
71
+ i18n_path: getLanguageBasePath(config, langKey),
72
+ i18n_lang: langKey,
73
+ lang: language.locale,
74
+ language: language.locale,
75
+ };
76
+
77
+ if (!post.i18n_key) {
78
+ update.i18n_key = getI18nKey(post) || inferI18nKeyFromSource(post.source);
79
+ }
80
+
81
+ return { $set: update };
82
+ }
83
+
84
+ function getLocalizedPagePath(page, langKey, config) {
85
+ const sourcePrefix = `${trimSlashes(langKey)}/`;
86
+ let route = page.path || "";
87
+
88
+ if (route.startsWith(sourcePrefix)) {
89
+ route = route.slice(sourcePrefix.length);
90
+ }
91
+
92
+ return trimSlashes(localizePath(`/${route}`, langKey, config));
93
+ }
94
+
95
+ function buildPageI18nUpdate(page, langKey, config) {
96
+ const language = getLanguage(config, langKey);
97
+ const update = {
98
+ i18n_path: getLanguageBasePath(config, langKey),
99
+ i18n_lang: langKey,
100
+ lang: language.locale,
101
+ language: language.locale,
102
+ path: getLocalizedPagePath(page, langKey, config),
103
+ };
104
+
105
+ if (!page.i18n_key) {
106
+ update.i18n_key = getI18nKey(page) || inferI18nKeyFromSource(page.source);
107
+ }
108
+
109
+ return { $set: update };
110
+ }
111
+
112
+ module.exports = (hexo) => {
113
+ const defaultPostProcessor = createPostProcessor(hexo);
114
+
115
+ hexo.extend.processor.register(
116
+ (sourcePath) => getLocalizedPostParams(hexo, sourcePath),
117
+ (file) =>
118
+ Promise.resolve(defaultPostProcessor.process(file)).then(() => {
119
+ if (!isI18nEnabled(getConfig(hexo))) return null;
120
+ if (!file.params.renderable) return null;
121
+
122
+ const post = hexo.model("Post").findOne({ source: file.path });
123
+ if (!post) return null;
124
+
125
+ return post.update(buildPostI18nUpdate(post, file.params.i18n_lang, getConfig(hexo)));
126
+ }),
127
+ );
128
+
129
+ hexo.extend.processor.register(
130
+ (sourcePath) => getLocalizedPageParams(hexo, sourcePath),
131
+ (file) => {
132
+ if (!isI18nEnabled(getConfig(hexo))) return null;
133
+ if (file.type === "delete") return null;
134
+
135
+ const page = hexo.model("Page").findOne({ source: file.path });
136
+ if (!page) return null;
137
+
138
+ return page.update(buildPageI18nUpdate(page, file.params.i18n_lang, getConfig(hexo)));
139
+ },
140
+ );
141
+
142
+ hexo.extend.filter.register(
143
+ "post_permalink",
144
+ (post) => {
145
+ if (!post || typeof post !== "object") return post;
146
+ const activeConfig = getConfig(hexo);
147
+ if (!isI18nEnabled(activeConfig)) return post;
148
+
149
+ const langKey = getPageLanguageKey(post, activeConfig);
150
+ const language = getLanguage(activeConfig, langKey);
151
+
152
+ post.i18n_lang = post.i18n_lang || langKey;
153
+ post.i18n_path = getLanguageBasePath(activeConfig, langKey);
154
+ post.lang = language.locale;
155
+ post.language = language.locale;
156
+ post.i18n_key = post.i18n_key || getI18nKey(post) || inferI18nKeyFromSource(post.source);
157
+
158
+ return post;
159
+ },
160
+ 5,
161
+ );
162
+
163
+ hexo.extend.filter.register(
164
+ "template_locals",
165
+ (locals) => {
166
+ const page = locals.page;
167
+ const activeConfig = getConfig(hexo, locals);
168
+ if (!page || !isI18nEnabled(activeConfig)) return locals;
169
+
170
+ const langKey = getPageLanguageKey(page, activeConfig);
171
+ const language = getLanguage(activeConfig, langKey);
172
+
173
+ page.i18n_lang = page.i18n_lang || langKey;
174
+ page.i18n_path = getLanguageBasePath(activeConfig, langKey);
175
+ page.lang = language.locale;
176
+ page.language = language.locale;
177
+ page.i18n_key = page.i18n_key || getI18nKey(page) || inferI18nKeyFromSource(page.source);
178
+
179
+ return locals;
180
+ },
181
+ 5,
182
+ );
183
+ };
@@ -0,0 +1,132 @@
1
+ const STORAGE_KEY = "gnix-article-font";
2
+
3
+ const DEFAULT_SETTINGS = Object.freeze({
4
+ size: "medium",
5
+ type: "serif",
6
+ lineHeight: 1.7,
7
+ weight: "regular",
8
+ });
9
+
10
+ const SIZE_OPTIONS = Object.freeze(["small", "medium-small", "medium", "medium-large", "large"]);
11
+ const FONT_OPTIONS = Object.freeze(["serif", "sans-serif", "mono", "handwriting"]);
12
+ const WEIGHT_OPTIONS = Object.freeze(["light", "regular", "medium"]);
13
+ const LINE_HEIGHT = Object.freeze({
14
+ min: 1.45,
15
+ max: 1.9,
16
+ });
17
+
18
+ const CUSTOM_FONT_FAMILY_OPTIONS = Object.freeze({
19
+ serif: "--font-serif",
20
+ "sans-serif": "--font-sans-serif",
21
+ mono: "--font-mono",
22
+ handwriting: "--font-handwriting",
23
+ });
24
+
25
+ const CUSTOM_FONT_IMPORT_LIMIT = 6;
26
+
27
+ function getClientArticleFontConfig() {
28
+ return {
29
+ storageKey: STORAGE_KEY,
30
+ defaultSettings: DEFAULT_SETTINGS,
31
+ sizeOptions: SIZE_OPTIONS,
32
+ fontOptions: FONT_OPTIONS,
33
+ weightOptions: WEIGHT_OPTIONS,
34
+ lineHeight: LINE_HEIGHT,
35
+ customFonts: {
36
+ familyOptions: CUSTOM_FONT_FAMILY_OPTIONS,
37
+ importLimit: CUSTOM_FONT_IMPORT_LIMIT,
38
+ },
39
+ };
40
+ }
41
+
42
+ function stringifyForScript(value) {
43
+ return JSON.stringify(value)
44
+ .replace(/</g, "\\u003c")
45
+ .replace(/\u2028/g, "\\u2028")
46
+ .replace(/\u2029/g, "\\u2029");
47
+ }
48
+
49
+ function getArticleFontInitScript() {
50
+ const config = stringifyForScript(getClientArticleFontConfig());
51
+
52
+ return `
53
+ (function() {
54
+ var config = ${config};
55
+ var defaults = config.defaultSettings || {};
56
+ var stored = null;
57
+ var parsed = {};
58
+ var utils = window.__GNIX_ARTICLE_FONT_UTILS__ || {};
59
+
60
+ window.__GNIX_ARTICLE_FONT_CONFIG__ = config;
61
+
62
+ try {
63
+ stored = localStorage.getItem(config.storageKey);
64
+ } catch (_) {}
65
+
66
+ if (stored) {
67
+ try {
68
+ parsed = JSON.parse(stored) || {};
69
+ } catch (_) {}
70
+ }
71
+
72
+ function hasOption(options, value) {
73
+ return Array.isArray(options) && options.indexOf(value) !== -1;
74
+ }
75
+
76
+ function normalizeLineHeight(value) {
77
+ if (value === "compact") return 1.55;
78
+ if (value === "normal") return 1.7;
79
+ if (value === "relaxed") return 1.85;
80
+
81
+ var parsedValue = Number(value);
82
+ var min = Number(config.lineHeight && config.lineHeight.min);
83
+ var max = Number(config.lineHeight && config.lineHeight.max);
84
+ var fallback = Number(defaults.lineHeight);
85
+
86
+ if (!Number.isFinite(parsedValue)) parsedValue = Number.isFinite(fallback) ? fallback : 1.7;
87
+ if (!Number.isFinite(min)) min = 1.45;
88
+ if (!Number.isFinite(max)) max = 1.9;
89
+
90
+ return Math.min(max, Math.max(min, parsedValue));
91
+ }
92
+
93
+ var candidate = Object.assign({}, defaults, parsed);
94
+ var customFonts = utils.normalizeCustomFonts
95
+ ? utils.normalizeCustomFonts(
96
+ candidate.customFonts,
97
+ config.customFonts && config.customFonts.familyOptions,
98
+ config.customFonts && config.customFonts.importLimit
99
+ )
100
+ : { imports: [], families: {} };
101
+ var settings = {
102
+ size: hasOption(config.sizeOptions, candidate.size) ? candidate.size : defaults.size,
103
+ type: hasOption(config.fontOptions, candidate.type) ? candidate.type : defaults.type,
104
+ lineHeight: normalizeLineHeight(candidate.lineHeight),
105
+ weight: hasOption(config.weightOptions, candidate.weight) ? candidate.weight : defaults.weight,
106
+ customFonts: customFonts
107
+ };
108
+ var html = document.documentElement;
109
+
110
+ if (utils.applyCustomFontImports) utils.applyCustomFontImports(settings.customFonts.imports);
111
+ if (utils.applyCustomFontFamilies) utils.applyCustomFontFamilies(html, settings.customFonts.families, config.customFonts && config.customFonts.familyOptions);
112
+ html.setAttribute("data-article-font-size", settings.size);
113
+ html.setAttribute("data-article-font-family", settings.type);
114
+ html.setAttribute("data-article-line-height", String(settings.lineHeight));
115
+ html.setAttribute("data-article-font-weight", settings.weight);
116
+ html.style.setProperty("--article-line-height", String(settings.lineHeight));
117
+ })();
118
+ `;
119
+ }
120
+
121
+ module.exports = {
122
+ CUSTOM_FONT_FAMILY_OPTIONS,
123
+ CUSTOM_FONT_IMPORT_LIMIT,
124
+ DEFAULT_SETTINGS,
125
+ FONT_OPTIONS,
126
+ LINE_HEIGHT,
127
+ SIZE_OPTIONS,
128
+ STORAGE_KEY,
129
+ WEIGHT_OPTIONS,
130
+ getArticleFontInitScript,
131
+ getClientArticleFontConfig,
132
+ };
@@ -0,0 +1,280 @@
1
+ const path = require("node:path");
2
+
3
+ const DEFAULT_LANGUAGES = {
4
+ cn: {
5
+ label: "Chinese",
6
+ locale: "zh-CN",
7
+ prefix: "cn",
8
+ },
9
+ en: {
10
+ label: "English",
11
+ locale: "en",
12
+ prefix: "en",
13
+ },
14
+ };
15
+
16
+ function trimSlashes(value) {
17
+ return String(value || "").replace(/^\/+|\/+$/g, "");
18
+ }
19
+
20
+ function normalizeLanguageKey(value) {
21
+ return trimSlashes(value).trim();
22
+ }
23
+
24
+ function normalizePrefix(value, fallback) {
25
+ const prefix = trimSlashes(value == null ? fallback : value);
26
+ return prefix ? `${prefix}/` : "";
27
+ }
28
+
29
+ function normalizeLocale(value) {
30
+ return String(value || "").replace(/_/g, "-");
31
+ }
32
+
33
+ function isExternalUrl(value) {
34
+ return /^(?:[a-z][a-z\d+.-]*:)?\/\//i.test(value) || /^(?:mailto|tel|data):/i.test(value) || value.startsWith("#");
35
+ }
36
+
37
+ const i18nConfigCache = new WeakMap();
38
+
39
+ function getI18nConfig(config = {}) {
40
+ if (i18nConfigCache.has(config)) {
41
+ return i18nConfigCache.get(config);
42
+ }
43
+
44
+ const configI18n = config.i18n || {};
45
+ const themeI18n = config.theme_config?.i18n || {};
46
+ const raw = configI18n.enabled === true || configI18n.languages ? configI18n : themeI18n.enabled === true || themeI18n.languages ? themeI18n : configI18n;
47
+ const rawLanguages = raw.languages || DEFAULT_LANGUAGES;
48
+ const languages = {};
49
+
50
+ Object.keys(rawLanguages).forEach((key) => {
51
+ const normalizedKey = normalizeLanguageKey(key);
52
+ const value = typeof rawLanguages[key] === "string" ? { locale: rawLanguages[key] } : rawLanguages[key] || {};
53
+ const locale = normalizeLocale(value.locale || value.lang || value.language || normalizedKey);
54
+ const prefix = value.prefix ?? value.path ?? value.url_prefix;
55
+ languages[normalizedKey] = {
56
+ key: normalizedKey,
57
+ label: value.label || value.name || locale || normalizedKey,
58
+ locale,
59
+ prefix: normalizePrefix(prefix, normalizedKey),
60
+ };
61
+ });
62
+
63
+ const keys = Object.keys(languages);
64
+ const configuredDefault = normalizeLanguageKey(raw.default || raw.default_language || raw.defaultLanguage || keys[0]);
65
+ const defaultLanguage = languages[configuredDefault] ? configuredDefault : keys[0];
66
+
67
+ const result = {
68
+ enabled: raw.enabled === true,
69
+ defaultLanguage,
70
+ languages,
71
+ };
72
+
73
+ i18nConfigCache.set(config, result);
74
+ return result;
75
+ }
76
+
77
+ function isI18nEnabled(config = {}) {
78
+ return getI18nConfig(config).enabled;
79
+ }
80
+
81
+ function getLanguageKeys(config = {}) {
82
+ return Object.keys(getI18nConfig(config).languages);
83
+ }
84
+
85
+ function getDefaultLanguageKey(config = {}) {
86
+ return getI18nConfig(config).defaultLanguage;
87
+ }
88
+
89
+ function getLanguage(config = {}, key) {
90
+ const i18n = getI18nConfig(config);
91
+ const normalizedKey = normalizeLanguageKey(key);
92
+ return i18n.languages[normalizedKey] || i18n.languages[i18n.defaultLanguage];
93
+ }
94
+
95
+ function getLanguageKeyFromLocale(config = {}, locale) {
96
+ const normalizedLocale = normalizeLocale(locale).toLowerCase();
97
+ if (!normalizedLocale) return null;
98
+
99
+ const i18n = getI18nConfig(config);
100
+ if (i18n.languages[normalizedLocale]) return normalizedLocale;
101
+
102
+ return (
103
+ Object.keys(i18n.languages).find((key) => {
104
+ const language = i18n.languages[key];
105
+ return language.locale.toLowerCase() === normalizedLocale;
106
+ }) || null
107
+ );
108
+ }
109
+
110
+ function getLanguageKeyFromPath(value, config = {}) {
111
+ const normalized = trimSlashes(value);
112
+ if (!normalized) return null;
113
+
114
+ const i18n = getI18nConfig(config);
115
+ return (
116
+ Object.keys(i18n.languages).find((key) => {
117
+ const prefix = trimSlashes(i18n.languages[key].prefix);
118
+ return prefix && (normalized === prefix || normalized.startsWith(`${prefix}/`));
119
+ }) || null
120
+ );
121
+ }
122
+
123
+ function getLanguageKeyFromSource(value, config = {}) {
124
+ const normalized = trimSlashes(value).replace(/\\/g, "/");
125
+ if (!normalized) return null;
126
+
127
+ const firstSegment = normalized.split("/")[0];
128
+ const i18n = getI18nConfig(config);
129
+ return i18n.languages[firstSegment] ? firstSegment : null;
130
+ }
131
+
132
+ function getPageLanguageKey(page = {}, config = {}) {
133
+ if (!isI18nEnabled(config)) {
134
+ const configuredLanguage = Array.isArray(config.language) ? config.language[0] : config.language;
135
+ return getLanguageKeyFromLocale(config, page.lang || page.language || configuredLanguage) || normalizeLanguageKey(page.lang || page.language || configuredLanguage) || getDefaultLanguageKey(config);
136
+ }
137
+
138
+ const explicit = normalizeLanguageKey(page.i18n_lang || page.i18n?.lang || page.i18n?.language);
139
+ if (explicit && getLanguage(config, explicit)?.key === explicit) return explicit;
140
+
141
+ const fromSource = getLanguageKeyFromSource(page.source || page.full_source || "", config);
142
+ if (fromSource) return fromSource;
143
+
144
+ const pathDescriptor = Object.getOwnPropertyDescriptor(page, "path");
145
+ const pathValue = pathDescriptor && typeof pathDescriptor.get !== "function" ? pathDescriptor.value : "";
146
+ const fromPath = getLanguageKeyFromPath(pathValue || page.canonical_path || "", config);
147
+ if (fromPath) return fromPath;
148
+
149
+ const fromLocale = getLanguageKeyFromLocale(config, page.lang || page.language);
150
+ if (fromLocale) return fromLocale;
151
+
152
+ return getDefaultLanguageKey(config);
153
+ }
154
+
155
+ function getPageLocale(page = {}, config = {}) {
156
+ if (!isI18nEnabled(config)) {
157
+ const configuredLanguage = Array.isArray(config.language) ? config.language[0] : config.language;
158
+ return normalizeLocale(page.lang || page.language || configuredLanguage || "");
159
+ }
160
+
161
+ const language = getLanguage(config, getPageLanguageKey(page, config));
162
+ return language?.locale || normalizeLocale(page.lang || page.language) || "";
163
+ }
164
+
165
+ function getLanguageLabel(config = {}, keyOrLocale) {
166
+ const key = getLanguage(config, keyOrLocale)?.key === normalizeLanguageKey(keyOrLocale) ? normalizeLanguageKey(keyOrLocale) : getLanguageKeyFromLocale(config, keyOrLocale);
167
+ const language = getLanguage(config, key || getDefaultLanguageKey(config));
168
+ return language?.label || keyOrLocale;
169
+ }
170
+
171
+ function getLanguageBasePath(config = {}, key) {
172
+ return getLanguage(config, key)?.prefix || "";
173
+ }
174
+
175
+ function joinRoute(...parts) {
176
+ const joined = parts.map(trimSlashes).filter(Boolean).join("/");
177
+ return joined ? `${joined}/` : "";
178
+ }
179
+
180
+ function stripLanguagePrefix(value, config = {}) {
181
+ const normalized = trimSlashes(value);
182
+ const key = getLanguageKeyFromPath(normalized, config);
183
+ if (!key) return normalized;
184
+
185
+ const prefix = trimSlashes(getLanguageBasePath(config, key));
186
+ return normalized.slice(prefix.length).replace(/^\/+/, "");
187
+ }
188
+
189
+ function localizePath(value, key, config = {}) {
190
+ if (!isI18nEnabled(config) || typeof value !== "string" || !value.trim() || isExternalUrl(value)) {
191
+ return value;
192
+ }
193
+
194
+ const [pathAndQuery, hash = ""] = value.split("#");
195
+ const [pathname, query = ""] = pathAndQuery.split("?");
196
+ const route = stripLanguagePrefix(pathname, config);
197
+ const base = trimSlashes(getLanguageBasePath(config, key || getDefaultLanguageKey(config)));
198
+ const normalizedRoute = trimSlashes(route);
199
+ const joined = [base, normalizedRoute].filter(Boolean).join("/");
200
+ const hasFileExtension = /\.[^/]+$/.test(normalizedRoute);
201
+ const localized = `/${joined}${joined && !hasFileExtension ? "/" : ""}`;
202
+ const queryPart = query ? `?${query}` : "";
203
+ const hashPart = hash ? `#${hash}` : "";
204
+ return `${localized}${queryPart}${hashPart}`;
205
+ }
206
+
207
+ function toArray(collection) {
208
+ if (!collection) return [];
209
+ if (typeof collection.toArray === "function") return collection.toArray();
210
+ if (Array.isArray(collection.data)) return collection.data;
211
+ if (Array.isArray(collection)) return collection;
212
+ return [];
213
+ }
214
+
215
+ function filterByLanguage(collection, key, config = {}) {
216
+ if (!isI18nEnabled(config)) return collection;
217
+ if (typeof collection?.filter === "function" && !Array.isArray(collection)) {
218
+ return collection.filter((item) => getPageLanguageKey(item, config) === key);
219
+ }
220
+ return toArray(collection).filter((item) => getPageLanguageKey(item, config) === key);
221
+ }
222
+
223
+ function getI18nKey(item = {}) {
224
+ return item.i18n_key || item.i18n?.key || item.translation_key || item.slug || inferI18nKeyFromSource(item.source);
225
+ }
226
+
227
+ function getLocalizedTagPath(tag, key, config = {}) {
228
+ const tagDir = config.tag_dir || "tags";
229
+ const tagPath = typeof tag === "string" ? tag : tag?.path || tag?.slug || tag?.name || "";
230
+ let slug = trimSlashes(tagPath);
231
+ const tagDirPrefix = `${trimSlashes(tagDir)}/`;
232
+
233
+ if (slug.startsWith(tagDirPrefix)) {
234
+ slug = slug.slice(tagDirPrefix.length);
235
+ }
236
+
237
+ return joinRoute(getLanguageBasePath(config, key), tagDir, slug);
238
+ }
239
+
240
+ function inferI18nKeyFromSource(source) {
241
+ if (typeof source !== "string") return "";
242
+ const normalized = trimSlashes(source).replace(/\\/g, "/");
243
+ const ext = path.posix.extname(normalized);
244
+ const withoutExt = ext ? normalized.slice(0, -ext.length) : normalized;
245
+ const parts = withoutExt.split("/").filter(Boolean);
246
+ if (!parts.length) return "";
247
+
248
+ const last = parts[parts.length - 1];
249
+ if (last === "index" && parts.length >= 2) {
250
+ return parts[parts.length - 2];
251
+ }
252
+
253
+ return last;
254
+ }
255
+
256
+ module.exports = {
257
+ filterByLanguage,
258
+ getDefaultLanguageKey,
259
+ getI18nConfig,
260
+ getI18nKey,
261
+ getLanguage,
262
+ getLanguageBasePath,
263
+ getLanguageKeyFromLocale,
264
+ getLanguageKeyFromPath,
265
+ getLanguageKeyFromSource,
266
+ getLanguageKeys,
267
+ getLanguageLabel,
268
+ getLocalizedTagPath,
269
+ getPageLanguageKey,
270
+ getPageLocale,
271
+ inferI18nKeyFromSource,
272
+ isExternalUrl,
273
+ isI18nEnabled,
274
+ joinRoute,
275
+ localizePath,
276
+ normalizeLocale,
277
+ stripLanguagePrefix,
278
+ toArray,
279
+ trimSlashes,
280
+ };
@@ -0,0 +1,84 @@
1
+ const STORAGE_KEY = "themePreference";
2
+ const DEFAULT_THEME = "system";
3
+
4
+ const SYSTEM_THEME = Object.freeze({
5
+ dark: "mocha",
6
+ light: "nord",
7
+ });
8
+
9
+ const THEME_OPTIONS = Object.freeze([
10
+ { label: "SYSTEM", name: "System", value: DEFAULT_THEME },
11
+ { label: "LATTE", name: "Catppuccin Latte", value: "latte", colorScheme: "light" },
12
+ { label: "NORD", name: "Nord Light", value: "nord", colorScheme: "light" },
13
+ { label: "SONG CI", name: "Song Porcelain", value: "song_ci", colorScheme: "light" },
14
+ { label: "NORD NIGHT", name: "Nord Night", value: "nord_night", colorScheme: "night" },
15
+ { label: "ROSE PINE", name: "Rosé Pine", value: "rose_pine", colorScheme: "night" },
16
+ { label: "MOCHA", name: "Catppuccin Mocha", value: "mocha", colorScheme: "night" },
17
+ { label: "TOKYO NIGHT", name: "Tokyo Night", value: "tokyo_night", colorScheme: "night" },
18
+ ]);
19
+
20
+ function getConcreteThemeOptions() {
21
+ return THEME_OPTIONS.filter((theme) => theme.value !== DEFAULT_THEME);
22
+ }
23
+
24
+ function getThemeClassMap() {
25
+ return Object.fromEntries(getConcreteThemeOptions().map((theme) => [theme.value, theme.colorScheme]));
26
+ }
27
+
28
+ function getClientThemeConfig() {
29
+ return {
30
+ storageKey: STORAGE_KEY,
31
+ defaultTheme: DEFAULT_THEME,
32
+ systemTheme: SYSTEM_THEME,
33
+ themes: THEME_OPTIONS,
34
+ themeClassMap: getThemeClassMap(),
35
+ };
36
+ }
37
+
38
+ function stringifyForScript(value) {
39
+ return JSON.stringify(value)
40
+ .replace(/</g, "\\u003c")
41
+ .replace(/\u2028/g, "\\u2028")
42
+ .replace(/\u2029/g, "\\u2029");
43
+ }
44
+
45
+ function getThemeInitScript() {
46
+ const config = stringifyForScript(getClientThemeConfig());
47
+
48
+ return `
49
+ (function() {
50
+ var config = ${config};
51
+ var themeClassMap = config.themeClassMap || {};
52
+ var stored = null;
53
+
54
+ window.__GNIX_THEME_CONFIG__ = config;
55
+
56
+ try {
57
+ stored = localStorage.getItem(config.storageKey);
58
+ } catch (_) {}
59
+
60
+ var theme = stored === config.defaultTheme || Object.prototype.hasOwnProperty.call(themeClassMap, stored)
61
+ ? stored
62
+ : config.defaultTheme;
63
+ var resolvedTheme = theme === config.defaultTheme
64
+ ? window.matchMedia("(prefers-color-scheme: dark)").matches ? config.systemTheme.dark : config.systemTheme.light
65
+ : theme;
66
+ var html = document.documentElement;
67
+ var themeClass = themeClassMap[resolvedTheme];
68
+
69
+ html.setAttribute("data-theme", resolvedTheme);
70
+ if (themeClass) html.classList.add(themeClass);
71
+ })();
72
+ `;
73
+ }
74
+
75
+ module.exports = {
76
+ DEFAULT_THEME,
77
+ STORAGE_KEY,
78
+ SYSTEM_THEME,
79
+ THEME_OPTIONS,
80
+ getClientThemeConfig,
81
+ getConcreteThemeOptions,
82
+ getThemeClassMap,
83
+ getThemeInitScript,
84
+ };