@anglefeint/astro-theme 0.1.29 → 0.1.30
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/package.json +2 -1
- package/src/scaffold/new-page.mjs +62 -0
- package/src/scaffold/new-post.mjs +102 -0
- package/src/scaffold/shared.mjs +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anglefeint/astro-theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Anglefeint core theme package for Astro",
|
|
6
6
|
"keywords": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"src/config",
|
|
24
24
|
"src/components",
|
|
25
25
|
"src/layouts",
|
|
26
|
+
"src/scaffold",
|
|
26
27
|
"src/scripts",
|
|
27
28
|
"src/i18n",
|
|
28
29
|
"src/styles",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toTitleFromSlug, validatePageSlug } from './shared.mjs';
|
|
2
|
+
|
|
3
|
+
export const THEMES = ['base', 'cyber', 'ai', 'hacker', 'matrix'];
|
|
4
|
+
|
|
5
|
+
export const LAYOUT_BY_THEME = {
|
|
6
|
+
base: 'BasePageLayout',
|
|
7
|
+
ai: 'AiPageLayout',
|
|
8
|
+
cyber: 'CyberPageLayout',
|
|
9
|
+
hacker: 'HackerPageLayout',
|
|
10
|
+
matrix: 'MatrixPageLayout',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function parseNewPageArgs(argv) {
|
|
14
|
+
const args = argv.slice(2);
|
|
15
|
+
const positional = [];
|
|
16
|
+
let theme = 'base';
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
19
|
+
const token = args[i];
|
|
20
|
+
if (token === '--theme') {
|
|
21
|
+
theme = args[i + 1] ?? '';
|
|
22
|
+
i += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
positional.push(token);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { slug: positional[0], theme };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isValidNewPageTheme(theme) {
|
|
32
|
+
return THEMES.includes(theme);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function usageNewPage() {
|
|
36
|
+
return 'Usage: npm run new-page -- <slug> [--theme <base|cyber|ai|hacker|matrix>]';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildNewPageTemplate({ slug, theme }) {
|
|
40
|
+
const title = toTitleFromSlug(slug) || 'New Page';
|
|
41
|
+
const layoutName = LAYOUT_BY_THEME[theme];
|
|
42
|
+
|
|
43
|
+
return `---
|
|
44
|
+
import type { GetStaticPaths } from 'astro';
|
|
45
|
+
import ${layoutName} from '@anglefeint/astro-theme/layouts/${layoutName}.astro';
|
|
46
|
+
import { SUPPORTED_LOCALES, type Locale } from '@anglefeint/site-i18n/config';
|
|
47
|
+
|
|
48
|
+
export const getStaticPaths = (() => SUPPORTED_LOCALES.map((lang: Locale) => ({ params: { lang } }))) satisfies GetStaticPaths;
|
|
49
|
+
|
|
50
|
+
const locale = Astro.params.lang as Locale;
|
|
51
|
+
const pageTitle = '${title}';
|
|
52
|
+
const pageDescription = 'A custom page built from the ${theme} theme shell.';
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<${layoutName} locale={locale} title={pageTitle} description={pageDescription}>
|
|
56
|
+
\t<h1>${title}</h1>
|
|
57
|
+
\t<p>Replace this content with your own page content.</p>
|
|
58
|
+
</${layoutName}>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { validatePageSlug };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { hashString, normalizePathForFrontmatter, toTitleFromSlug, validatePostSlug } from './shared.mjs';
|
|
4
|
+
|
|
5
|
+
export function usageNewPost() {
|
|
6
|
+
return 'Usage: npm run new-post -- <slug> [--locales en,ja,ko,es,zh]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseNewPostArgs(argv) {
|
|
10
|
+
const args = argv.slice(2);
|
|
11
|
+
const positional = [];
|
|
12
|
+
let locales = '';
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
15
|
+
const token = args[i];
|
|
16
|
+
if (token === '--locales') {
|
|
17
|
+
locales = args[i + 1] ?? '';
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
positional.push(token);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { slug: positional[0], locales };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveLocales({ cliLocales, envLocales, defaultLocales }) {
|
|
28
|
+
const raw = cliLocales || envLocales || '';
|
|
29
|
+
if (!raw) return [...defaultLocales];
|
|
30
|
+
|
|
31
|
+
const parsed = raw
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((locale) => locale.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((locale) => locale.toLowerCase());
|
|
36
|
+
|
|
37
|
+
if (parsed.length === 0) {
|
|
38
|
+
throw new Error('Locales list is empty. Example: --locales en,ja,ko');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const localePattern = /^[a-z]{2,3}(?:-[a-z0-9]+)?$/;
|
|
42
|
+
const invalid = parsed.find((locale) => !localePattern.test(locale));
|
|
43
|
+
if (invalid) {
|
|
44
|
+
throw new Error(`Invalid locale "${invalid}".`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Array.from(new Set(parsed));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildNewPostTemplate(locale, slug, pubDate, heroImage) {
|
|
51
|
+
const titleByLocale = {
|
|
52
|
+
en: toTitleFromSlug(slug),
|
|
53
|
+
ja: '新しい記事タイトル',
|
|
54
|
+
ko: '새 글 제목',
|
|
55
|
+
es: 'Título del nuevo artículo',
|
|
56
|
+
zh: '新文章标题',
|
|
57
|
+
};
|
|
58
|
+
const descriptionByLocale = {
|
|
59
|
+
en: `A short EN post scaffold for "${slug}".`,
|
|
60
|
+
ja: `「${slug}」用の短い日本語記事テンプレートです。`,
|
|
61
|
+
ko: `"${slug}"용 한국어 글 템플릿입니다.`,
|
|
62
|
+
es: `Plantilla breve en español para "${slug}".`,
|
|
63
|
+
zh: `“${slug}”的中文文章模板。`,
|
|
64
|
+
};
|
|
65
|
+
const bodyByLocale = {
|
|
66
|
+
en: `Write your EN content for "${slug}" here.`,
|
|
67
|
+
ja: `ここに「${slug}」の日本語本文を書いてください。`,
|
|
68
|
+
ko: `여기에 "${slug}" 한국어 본문을 작성하세요.`,
|
|
69
|
+
es: `Escribe aquí el contenido en español para "${slug}".`,
|
|
70
|
+
zh: `请在这里填写“${slug}”的中文正文。`,
|
|
71
|
+
};
|
|
72
|
+
return `---
|
|
73
|
+
title: '${titleByLocale[locale]}'
|
|
74
|
+
subtitle: ''
|
|
75
|
+
description: '${descriptionByLocale[locale]}'
|
|
76
|
+
pubDate: '${pubDate}'
|
|
77
|
+
${heroImage ? `heroImage: '${heroImage}'` : ''}
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
${bodyByLocale[locale]}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function loadDefaultCovers(defaultCoversRoot) {
|
|
85
|
+
try {
|
|
86
|
+
const entries = await readdir(defaultCoversRoot, { withFileTypes: true });
|
|
87
|
+
return entries
|
|
88
|
+
.filter((entry) => entry.isFile() && /\.(webp|png|jpe?g)$/i.test(entry.name))
|
|
89
|
+
.map((entry) => path.join(defaultCoversRoot, entry.name))
|
|
90
|
+
.sort((a, b) => a.localeCompare(b));
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function pickDefaultCoverBySlug(slug, localeDir, defaultCovers) {
|
|
97
|
+
if (defaultCovers.length === 0) return '';
|
|
98
|
+
const coverPath = defaultCovers[hashString(slug) % defaultCovers.length];
|
|
99
|
+
return normalizePathForFrontmatter(path.relative(localeDir, coverPath));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { validatePostSlug };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function toTitleFromSlug(slug) {
|
|
4
|
+
return slug
|
|
5
|
+
.split('-')
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
|
|
8
|
+
.join(' ');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validatePostSlug(slug) {
|
|
12
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validatePageSlug(slug) {
|
|
16
|
+
if (!slug) return false;
|
|
17
|
+
if (slug.startsWith('/') || slug.endsWith('/')) return false;
|
|
18
|
+
const parts = slug.split('/');
|
|
19
|
+
return parts.every((part) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(part));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hashString(input) {
|
|
23
|
+
let hash = 5381;
|
|
24
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
25
|
+
hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
|
|
26
|
+
}
|
|
27
|
+
return hash >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizePathForFrontmatter(filePath) {
|
|
31
|
+
return filePath.split(path.sep).join('/');
|
|
32
|
+
}
|