@anglefeint/astro-theme 0.1.29 → 0.1.31
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/config/about.ts +84 -79
- 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.31",
|
|
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",
|
package/src/config/about.ts
CHANGED
|
@@ -3,83 +3,88 @@
|
|
|
3
3
|
* Used by src/pages/[lang]/about.astro and packages/theme/src/scripts/about-effects.js.
|
|
4
4
|
*/
|
|
5
5
|
export const ABOUT_CONFIG = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
6
|
+
metaLine: '$ profile booted | mode: builder',
|
|
7
|
+
sections: {
|
|
8
|
+
who: 'Write a short introduction about yourself, your background, and your primary focus areas.',
|
|
9
|
+
what: 'Describe what you build, your core skills, and the kinds of projects you want to be known for.',
|
|
10
|
+
ethos: [
|
|
11
|
+
'Prioritize clarity before complexity.',
|
|
12
|
+
'Favor maintainable systems over one-off solutions.',
|
|
13
|
+
'Ship in small iterations and learn from feedback.',
|
|
14
|
+
'Communicate directly and document decisions.',
|
|
15
|
+
],
|
|
16
|
+
now: 'Share what you are currently building, shipping, or learning.',
|
|
17
|
+
contactLead:
|
|
18
|
+
'Add a short collaboration note (for example: open to freelance, consulting, or full-time roles).',
|
|
19
|
+
signature: '> Replace with your own signature.',
|
|
20
|
+
},
|
|
21
|
+
contact: {
|
|
22
|
+
email: 'you@example.com',
|
|
23
|
+
githubUrl: 'https://github.com/yourname',
|
|
24
|
+
githubLabel: 'GitHub',
|
|
25
|
+
},
|
|
26
|
+
sidebar: {
|
|
27
|
+
dlData: 'DL Data',
|
|
28
|
+
ai: 'AI',
|
|
29
|
+
decryptor: 'Decryptor',
|
|
30
|
+
help: 'Help',
|
|
31
|
+
allScripts: 'All Scripts',
|
|
32
|
+
},
|
|
33
|
+
scriptsPath: '/root/bash/scripts',
|
|
34
|
+
modals: {
|
|
35
|
+
dlData: {
|
|
36
|
+
title: 'Downloading...',
|
|
37
|
+
subtitle: 'Critical Data',
|
|
38
|
+
},
|
|
39
|
+
ai: {
|
|
40
|
+
title: 'AI',
|
|
41
|
+
lines: [
|
|
42
|
+
'~ $ ai --status --verbose',
|
|
43
|
+
'',
|
|
44
|
+
'model: anglefeint-core',
|
|
45
|
+
'mode: reasoning + builder',
|
|
46
|
+
'context window: 128k',
|
|
47
|
+
'tools: codex / cursor / claude-code',
|
|
48
|
+
'latency: 120-220ms',
|
|
49
|
+
'safety: guardrails enabled',
|
|
50
|
+
'',
|
|
51
|
+
'>> system online',
|
|
52
|
+
'>> ready for execution',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
decryptor: {
|
|
56
|
+
title: 'Password Decryptor',
|
|
57
|
+
header: 'Calculating Hashes',
|
|
58
|
+
keysLabel: 'keys tested',
|
|
59
|
+
currentPassphraseLabel: 'Current passphrase:',
|
|
60
|
+
masterKeyLabel: 'Master key',
|
|
61
|
+
transientKeyLabel: 'Transient key',
|
|
62
|
+
},
|
|
63
|
+
help: {
|
|
64
|
+
title: 'Help',
|
|
65
|
+
statsLabel: 'Stats & Achievements',
|
|
66
|
+
typedPrefix: 'You typed:',
|
|
67
|
+
typedSuffix: 'characters',
|
|
68
|
+
},
|
|
69
|
+
allScripts: {
|
|
70
|
+
title: '/root/bash/scripts',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
effects: {
|
|
74
|
+
backgroundLines: [
|
|
75
|
+
'~ $ ls -la',
|
|
76
|
+
'total 42',
|
|
77
|
+
'drwxr-xr-x 12 user staff 384 Jan 12 about blog projects',
|
|
78
|
+
'drwxr-xr-x 8 user staff 256 Jan 11 .config .ssh keys',
|
|
79
|
+
'-rw-r--r-- 1 user staff 2048 Jan 10 README.md .env.gpg',
|
|
80
|
+
'-rwxr-xr-x 1 user staff 512 Jan 9 deploy.sh script',
|
|
81
|
+
'~ $ cat .motd',
|
|
82
|
+
'>> welcome | access granted',
|
|
83
|
+
],
|
|
84
|
+
scrollToasts: {
|
|
85
|
+
p30: 'context parsed',
|
|
86
|
+
p60: 'inference stable',
|
|
87
|
+
p90: 'output finalized',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
85
90
|
} as const;
|
|
@@ -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
|
+
}
|