@anglefeint/astro-theme 0.1.38 → 0.1.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -0,0 +1,48 @@
1
+ ---
2
+ type GiscusComments = {
3
+ REPO: string;
4
+ REPO_ID: string;
5
+ CATEGORY: string;
6
+ CATEGORY_ID: string;
7
+ MAPPING: string;
8
+ TERM: string;
9
+ NUMBER: string;
10
+ STRICT: string;
11
+ REACTIONS_ENABLED: string;
12
+ EMIT_METADATA: string;
13
+ INPUT_POSITION: string;
14
+ THEME: string;
15
+ LANG: string;
16
+ LOADING: string;
17
+ CROSSORIGIN: string;
18
+ };
19
+
20
+ type Props = {
21
+ comments: GiscusComments;
22
+ resolvedLocale: string;
23
+ };
24
+
25
+ const { comments, resolvedLocale } = Astro.props;
26
+ const normalizedCommentTerm = comments.TERM.trim();
27
+ const normalizedCommentNumber = comments.NUMBER.trim();
28
+ ---
29
+
30
+ <script
31
+ is:inline
32
+ src="https://giscus.app/client.js"
33
+ data-repo={comments.REPO}
34
+ data-repo-id={comments.REPO_ID}
35
+ data-category={comments.CATEGORY}
36
+ data-category-id={comments.CATEGORY_ID}
37
+ data-mapping={comments.MAPPING}
38
+ data-term={comments.MAPPING === 'specific' ? normalizedCommentTerm : undefined}
39
+ data-number={comments.MAPPING === 'number' ? normalizedCommentNumber : undefined}
40
+ data-strict={comments.STRICT}
41
+ data-reactions-enabled={comments.REACTIONS_ENABLED}
42
+ data-emit-metadata={comments.EMIT_METADATA}
43
+ data-input-position={comments.INPUT_POSITION}
44
+ data-theme={comments.THEME}
45
+ data-lang={comments.LANG || resolvedLocale}
46
+ data-loading={comments.LOADING}
47
+ crossorigin={comments.CROSSORIGIN}
48
+ async></script>
@@ -2,33 +2,58 @@ import { defineCollection } from 'astro:content';
2
2
  import { glob } from 'astro/loaders';
3
3
  import { z } from 'astro/zod';
4
4
 
5
+ const ABSOLUTE_URL_SCHEME_REGEX = /^[a-z][a-z\d+.-]*:/i;
6
+
7
+ function normalizeSourceLink(value: string): string {
8
+ const trimmed = value.trim();
9
+ return ABSOLUTE_URL_SCHEME_REGEX.test(trimmed) ? trimmed : `https://${trimmed}`;
10
+ }
11
+
12
+ function isValidSourceLink(value: string): boolean {
13
+ try {
14
+ const url = new URL(value);
15
+ return url.protocol === 'http:' || url.protocol === 'https:';
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ const sourceLinkSchema = z
22
+ .string()
23
+ .trim()
24
+ .min(1)
25
+ .transform(normalizeSourceLink)
26
+ .refine(isValidSourceLink, {
27
+ message: 'sourceLinks entries must be valid HTTP(S) URLs or bare domains.',
28
+ });
29
+
5
30
  const blog = defineCollection({
6
- // Load Markdown and MDX files in the `src/content/blog/` directory.
7
- loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
8
- // Type-check frontmatter using a schema
9
- schema: ({ image }) =>
10
- z.object({
11
- title: z.string(),
12
- subtitle: z.string().optional(),
13
- description: z.string(),
14
- // Transform string to Date object
15
- pubDate: z.coerce.date(),
16
- updatedDate: z.coerce.date().optional(),
17
- heroImage: image().optional(),
18
- context: z.string().optional(),
19
- readMinutes: z.number().int().positive().optional(),
20
- aiModel: z.string().optional(),
21
- aiMode: z.string().optional(),
22
- aiState: z.string().optional(),
23
- aiLatencyMs: z.number().int().nonnegative().optional(),
24
- aiConfidence: z.number().min(0).max(1).optional(),
25
- wordCount: z.number().int().nonnegative().optional(),
26
- tokenCount: z.number().int().nonnegative().optional(),
27
- author: z.string().optional(),
28
- tags: z.array(z.string()).optional(),
29
- canonicalTopic: z.string().optional(),
30
- sourceLinks: z.array(z.url()).optional(),
31
- }),
31
+ // Load Markdown and MDX files in the `src/content/blog/` directory.
32
+ loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
33
+ // Type-check frontmatter using a schema
34
+ schema: ({ image }) =>
35
+ z.object({
36
+ title: z.string(),
37
+ subtitle: z.string().optional(),
38
+ description: z.string(),
39
+ // Transform string to Date object
40
+ pubDate: z.coerce.date(),
41
+ updatedDate: z.coerce.date().optional(),
42
+ heroImage: image().optional(),
43
+ context: z.string().optional(),
44
+ readMinutes: z.number().int().positive().optional(),
45
+ aiModel: z.string().optional(),
46
+ aiMode: z.string().optional(),
47
+ aiState: z.string().optional(),
48
+ aiLatencyMs: z.number().int().nonnegative().optional(),
49
+ aiConfidence: z.number().min(0).max(1).optional(),
50
+ wordCount: z.number().int().nonnegative().optional(),
51
+ tokenCount: z.number().int().nonnegative().optional(),
52
+ author: z.string().optional(),
53
+ tags: z.array(z.string()).optional(),
54
+ canonicalTopic: z.string().optional(),
55
+ sourceLinks: z.array(sourceLinkSchema).optional(),
56
+ }),
32
57
  });
33
58
 
34
59
  export const collections = { blog };
@@ -48,6 +48,9 @@ export type Messages = {
48
48
  metaUpdated: string;
49
49
  metaReadMinutes: string;
50
50
  systemStatusAria: string;
51
+ systemModelLabel: string;
52
+ systemModeLabel: string;
53
+ systemStateLabel: string;
51
54
  promptContextLabel: string;
52
55
  latencyLabel: string;
53
56
  confidenceLabel: string;
@@ -116,6 +119,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
116
119
  metaUpdated: 'updated',
117
120
  metaReadMinutes: 'min read',
118
121
  systemStatusAria: 'Model status',
122
+ systemModelLabel: 'model',
123
+ systemModeLabel: 'mode',
124
+ systemStateLabel: 'state',
119
125
  promptContextLabel: 'Context',
120
126
  latencyLabel: 'latency est',
121
127
  confidenceLabel: 'confidence',
@@ -182,6 +188,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
182
188
  metaUpdated: '更新',
183
189
  metaReadMinutes: '分で読了',
184
190
  systemStatusAria: 'モデル状態',
191
+ systemModelLabel: 'モデル',
192
+ systemModeLabel: 'モード',
193
+ systemStateLabel: '状態',
185
194
  promptContextLabel: 'コンテキスト',
186
195
  latencyLabel: '推定レイテンシ',
187
196
  confidenceLabel: '信頼度',
@@ -248,6 +257,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
248
257
  metaUpdated: '수정',
249
258
  metaReadMinutes: '분 읽기',
250
259
  systemStatusAria: '모델 상태',
260
+ systemModelLabel: '모델',
261
+ systemModeLabel: '모드',
262
+ systemStateLabel: '상태',
251
263
  promptContextLabel: '컨텍스트',
252
264
  latencyLabel: '지연 추정',
253
265
  confidenceLabel: '신뢰도',
@@ -315,6 +327,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
315
327
  metaUpdated: 'actualizado',
316
328
  metaReadMinutes: 'min de lectura',
317
329
  systemStatusAria: 'Estado del modelo',
330
+ systemModelLabel: 'modelo',
331
+ systemModeLabel: 'modo',
332
+ systemStateLabel: 'estado',
318
333
  promptContextLabel: 'Contexto',
319
334
  latencyLabel: 'latencia est',
320
335
  confidenceLabel: 'confianza',
@@ -381,6 +396,9 @@ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
381
396
  metaUpdated: '更新',
382
397
  metaReadMinutes: '分钟阅读',
383
398
  systemStatusAria: '模型状态',
399
+ systemModelLabel: '模型',
400
+ systemModeLabel: '模式',
401
+ systemStateLabel: '状态',
384
402
  promptContextLabel: '语境',
385
403
  latencyLabel: '延迟估计',
386
404
  confidenceLabel: '置信度',
@@ -4,6 +4,7 @@ import type { CollectionEntry } from 'astro:content';
4
4
  import themeRedqueen1 from '../assets/theme/red-queen/theme-redqueen1.webp';
5
5
  import themeRedqueen2 from '../assets/theme/red-queen/theme-redqueen2.gif';
6
6
  import FormattedDate from '../components/FormattedDate.astro';
7
+ import Giscus from '../components/Giscus.astro';
7
8
  import AiShell from './shells/AiShell.astro';
8
9
  import blogPostCssUrl from '../styles/blog-post.css?url';
9
10
  import { SITE_AUTHOR } from '@anglefeint/site-config/site';
@@ -58,11 +59,16 @@ const hasStats = aiModel || wordCount !== undefined || tokenCount !== undefined;
58
59
  const confidenceText = aiConfidence !== undefined ? aiConfidence.toFixed(2) : undefined;
59
60
  const enableRedQueen = THEME.EFFECTS.ENABLE_RED_QUEEN;
60
61
  const comments = THEME.COMMENTS;
62
+ const normalizedCommentTerm = comments.TERM.trim();
63
+ const normalizedCommentNumber = comments.NUMBER.trim();
64
+ const hasValidCommentNumber =
65
+ /^[1-9]\d*$/.test(normalizedCommentNumber) &&
66
+ Number.isSafeInteger(Number(normalizedCommentNumber));
61
67
  const hasMappingParam =
62
68
  comments.MAPPING === 'specific'
63
- ? Boolean(comments.TERM)
69
+ ? Boolean(normalizedCommentTerm)
64
70
  : comments.MAPPING === 'number'
65
- ? Boolean(comments.NUMBER)
71
+ ? hasValidCommentNumber
66
72
  : true;
67
73
  const hasCommentsConfig = Boolean(
68
74
  comments.ENABLED &&
@@ -211,9 +217,21 @@ const hasCommentsConfig = Boolean(
211
217
  {
212
218
  hasSystemMeta && (
213
219
  <div class="ai-system-row" aria-label={messages.blog.systemStatusAria}>
214
- {aiModel && <span class="ai-system-chip">model: {aiModel}</span>}
215
- {aiMode && <span class="ai-system-chip">mode: {aiMode}</span>}
216
- {aiState && <span class="ai-system-chip">state: {aiState}</span>}
220
+ {aiModel && (
221
+ <span class="ai-system-chip">
222
+ {messages.blog.systemModelLabel}: {aiModel}
223
+ </span>
224
+ )}
225
+ {aiMode && (
226
+ <span class="ai-system-chip">
227
+ {messages.blog.systemModeLabel}: {aiMode}
228
+ </span>
229
+ )}
230
+ {aiState && (
231
+ <span class="ai-system-chip">
232
+ {messages.blog.systemStateLabel}: {aiState}
233
+ </span>
234
+ )}
217
235
  </div>
218
236
  )
219
237
  }
@@ -318,25 +336,7 @@ const hasCommentsConfig = Boolean(
318
336
  {
319
337
  hasCommentsConfig && (
320
338
  <section class="prose ai-comments" aria-label={messages.blog.comments}>
321
- <script
322
- src="https://giscus.app/client.js"
323
- data-repo={comments.REPO}
324
- data-repo-id={comments.REPO_ID}
325
- data-category={comments.CATEGORY}
326
- data-category-id={comments.CATEGORY_ID}
327
- data-mapping={comments.MAPPING}
328
- data-term={comments.MAPPING === 'specific' ? comments.TERM : undefined}
329
- data-number={comments.MAPPING === 'number' ? comments.NUMBER : undefined}
330
- data-strict={comments.STRICT}
331
- data-reactions-enabled={comments.REACTIONS_ENABLED}
332
- data-emit-metadata={comments.EMIT_METADATA}
333
- data-input-position={comments.INPUT_POSITION}
334
- data-theme={comments.THEME}
335
- data-lang={comments.LANG || resolvedLocale}
336
- data-loading={comments.LOADING}
337
- crossorigin={comments.CROSSORIGIN}
338
- async
339
- />
339
+ <Giscus comments={comments} resolvedLocale={resolvedLocale} />
340
340
  </section>
341
341
  )
342
342
  }
@@ -2,13 +2,18 @@ import { createDecryptorController } from './modal-decryptor.js';
2
2
  import { mountHelpKeyboard } from './modal-keyboard.js';
3
3
  import { renderProgressModal } from './modal-progress.js';
4
4
 
5
+ let cleanupAboutModals = null;
6
+
5
7
  export function initAboutModals(runtimeConfig, prefersReducedMotion) {
8
+ if (cleanupAboutModals) cleanupAboutModals();
9
+
6
10
  const modalOverlay = document.getElementById('hacker-modal');
7
11
  const modalBody = document.getElementById('hacker-modal-body');
8
12
  const modalTitle = document.querySelector('.hacker-modal-title');
9
13
  if (!modalOverlay || !modalBody || !modalTitle) return;
10
14
 
11
- const decryptorKeysLabel = runtimeConfig.decryptorKeysLabel || 'keys tested';
15
+ const decryptorKeysLabel =
16
+ typeof runtimeConfig.decryptorKeysLabel === 'string' ? runtimeConfig.decryptorKeysLabel : '';
12
17
  const decryptor = createDecryptorController(
13
18
  modalOverlay,
14
19
  prefersReducedMotion,
@@ -17,26 +22,8 @@ export function initAboutModals(runtimeConfig, prefersReducedMotion) {
17
22
  let cleanupKeyboard = null;
18
23
 
19
24
  const scriptsTpl = document.getElementById('hacker-scripts-folders-tpl');
20
- const fallbackModalContent = {
21
- 'dl-data': {
22
- title: 'Downloading...',
23
- body: '<div class="hacker-modal-download"><div class="modal-subtitle">Critical Data</div><div class="hacker-modal-progress" id="dl-progress"></div></div>',
24
- type: 'progress',
25
- },
26
- ai: {
27
- title: 'AI',
28
- body: '<pre>~ $ ai --status --verbose\n\nmodel: runtime-default\nmode: standard\ncontext window: 32k\nlatency: 100-250ms\nsafety: enabled\n\n&gt;&gt; system online\n&gt;&gt; ready</pre>',
29
- type: 'plain',
30
- },
31
- decryptor: {
32
- title: 'Password Decryptor',
33
- body: '<pre class="hacker-decryptor-pre">Calculating Hashes\n\n<span id="dec-keys">[00:00:01] 0 keys tested</span>\n\nCurrent passphrase: <span id="dec-pass">********</span>\n\nMaster key\n<span id="dec-master1"></span>\n<span id="dec-master2"></span>\n\nTransient key\n<span id="dec-trans1"></span>\n<span id="dec-trans2"></span>\n<span id="dec-trans3"></span>\n<span id="dec-trans4"></span></pre>',
34
- type: 'decryptor',
35
- },
36
- help: { title: 'Help', body: '', type: 'keyboard' },
37
- 'all-scripts': { title: '/root/bash/scripts', body: '', type: 'scripts' },
38
- };
39
- const modalContent = runtimeConfig.modalContent || fallbackModalContent;
25
+ const modalContent = runtimeConfig.modalContent;
26
+ if (!modalContent || typeof modalContent !== 'object') return;
40
27
 
41
28
  const closeModal = () => {
42
29
  decryptor.stop();
@@ -99,22 +86,43 @@ export function initAboutModals(runtimeConfig, prefersReducedMotion) {
99
86
  modalOverlay.setAttribute('aria-hidden', 'false');
100
87
  };
101
88
 
102
- document.querySelectorAll('.hacker-folder[data-modal]').forEach((button) => {
103
- button.addEventListener('click', () => {
89
+ const folderButtons = Array.from(document.querySelectorAll('.hacker-folder[data-modal]'));
90
+ const buttonHandlers = folderButtons.map((button) => {
91
+ const onClick = () => {
104
92
  const id = button.getAttribute('data-modal');
105
93
  if (!id) return;
106
94
  openModal(modalContent[id]);
107
- });
95
+ };
96
+ button.addEventListener('click', onClick);
97
+ return [button, onClick];
108
98
  });
109
99
 
110
100
  const closeButton = document.querySelector('.hacker-modal-close');
111
101
  if (closeButton) closeButton.addEventListener('click', closeModal);
112
102
 
113
- modalOverlay.addEventListener('click', (event) => {
103
+ const onOverlayClick = (event) => {
114
104
  if (event.target === modalOverlay) closeModal();
115
- });
105
+ };
106
+ modalOverlay.addEventListener('click', onOverlayClick);
116
107
 
117
- document.addEventListener('keydown', (event) => {
108
+ const onDocumentKeydown = (event) => {
118
109
  if (event.key === 'Escape' && modalOverlay.classList.contains('open')) closeModal();
119
- });
110
+ };
111
+ document.addEventListener('keydown', onDocumentKeydown);
112
+
113
+ cleanupAboutModals = () => {
114
+ decryptor.stop();
115
+ if (cleanupKeyboard) {
116
+ cleanupKeyboard();
117
+ cleanupKeyboard = null;
118
+ }
119
+ if (closeButton) closeButton.removeEventListener('click', closeModal);
120
+ modalOverlay.removeEventListener('click', onOverlayClick);
121
+ document.removeEventListener('keydown', onDocumentKeydown);
122
+ buttonHandlers.forEach(([button, handler]) => {
123
+ button.removeEventListener('click', handler);
124
+ });
125
+ };
126
+
127
+ return cleanupAboutModals;
120
128
  }
@@ -1,14 +1,15 @@
1
1
  export function initPostInteractions(prefersReducedMotion) {
2
- var glow = document.querySelector('.ai-mouse-glow');
3
- if (glow) {
4
- var raf;
5
- var x = 0;
6
- var y = 0;
7
- document.addEventListener('mousemove', function(e) {
2
+ const glow = document.querySelector('.ai-mouse-glow');
3
+ if (glow && window.__anglefeintPostGlowBound__ !== true) {
4
+ window.__anglefeintPostGlowBound__ = true;
5
+ let raf = 0;
6
+ let x = 0;
7
+ let y = 0;
8
+ document.addEventListener('mousemove', function (e) {
8
9
  x = e.clientX;
9
10
  y = e.clientY;
10
11
  if (!raf) {
11
- raf = requestAnimationFrame(function() {
12
+ raf = requestAnimationFrame(function () {
12
13
  glow.style.setProperty('--mouse-x', x + 'px');
13
14
  glow.style.setProperty('--mouse-y', y + 'px');
14
15
  raf = 0;
@@ -17,42 +18,56 @@ export function initPostInteractions(prefersReducedMotion) {
17
18
  });
18
19
  }
19
20
 
20
- document.querySelectorAll('.ai-prose-body a[href]').forEach(function(a) {
21
+ document.querySelectorAll('.ai-prose-body a[href]').forEach(function (a) {
22
+ if (a.dataset.aiLinkPreviewBound === 'true') return;
21
23
  var href = a.getAttribute('href') || '';
22
24
  if (!href || href.startsWith('#')) return;
25
+ a.dataset.aiLinkPreviewBound = 'true';
23
26
  a.classList.add('ai-link-preview');
24
27
  try {
25
- a.setAttribute('data-preview', href.startsWith('http') ? new URL(href, location.origin).hostname : href);
28
+ a.setAttribute(
29
+ 'data-preview',
30
+ href.startsWith('http') ? new URL(href, location.origin).hostname : href
31
+ );
26
32
  } catch (_err) {
27
33
  a.setAttribute('data-preview', href);
28
34
  }
29
35
  });
30
36
 
31
- var paras = document.querySelectorAll('.ai-prose-body p, .ai-prose-body h2, .ai-prose-body h3, .ai-prose-body pre, .ai-prose-body blockquote, .ai-prose-body ul, .ai-prose-body ol');
37
+ const paras = document.querySelectorAll(
38
+ '.ai-prose-body p, .ai-prose-body h2, .ai-prose-body h3, .ai-prose-body pre, .ai-prose-body blockquote, .ai-prose-body ul, .ai-prose-body ol'
39
+ );
32
40
  if (window.IntersectionObserver) {
33
- var io = new IntersectionObserver(function(entries) {
34
- entries.forEach(function(entry) {
35
- if (entry.isIntersecting) {
36
- entry.target.classList.add('ai-para-visible');
37
- io.unobserve(entry.target);
38
- }
39
- });
40
- }, { rootMargin: '0px 0px -60px 0px', threshold: 0.1 });
41
+ const io = new IntersectionObserver(
42
+ function (entries) {
43
+ entries.forEach(function (entry) {
44
+ if (entry.isIntersecting) {
45
+ entry.target.classList.add('ai-para-visible');
46
+ io.unobserve(entry.target);
47
+ }
48
+ });
49
+ },
50
+ { rootMargin: '0px 0px -60px 0px', threshold: 0.1 }
51
+ );
41
52
 
42
- paras.forEach(function(p) {
53
+ paras.forEach(function (p) {
54
+ if (p.dataset.aiRevealObserved === 'true') return;
55
+ p.dataset.aiRevealObserved = 'true';
43
56
  io.observe(p);
44
57
  });
45
58
  } else {
46
- paras.forEach(function(p) {
59
+ paras.forEach(function (p) {
47
60
  p.classList.add('ai-para-visible');
48
61
  });
49
62
  }
50
63
 
51
- var regen = document.querySelector('.ai-regenerate');
52
- var article = document.querySelector('.ai-article');
53
- var scan = document.querySelector('.ai-load-scan');
64
+ const regen = document.querySelector('.ai-regenerate');
65
+ const article = document.querySelector('.ai-article');
66
+ const scan = document.querySelector('.ai-load-scan');
54
67
  if (regen && article) {
55
- regen.addEventListener('click', function() {
68
+ if (regen.dataset.aiRegenerateBound === 'true') return;
69
+ regen.dataset.aiRegenerateBound = 'true';
70
+ regen.addEventListener('click', function () {
56
71
  regen.disabled = true;
57
72
  regen.classList.add('ai-regenerating');
58
73
  article.classList.add('ai-regenerate-flash');
@@ -63,11 +78,14 @@ export function initPostInteractions(prefersReducedMotion) {
63
78
  scan.style.top = '0';
64
79
  scan.style.opacity = '1';
65
80
  }
66
- setTimeout(function() {
67
- article.classList.remove('ai-regenerate-flash');
68
- regen.classList.remove('ai-regenerating');
69
- regen.disabled = false;
70
- }, prefersReducedMotion ? 120 : 1200);
81
+ setTimeout(
82
+ function () {
83
+ article.classList.remove('ai-regenerate-flash');
84
+ regen.classList.remove('ai-regenerating');
85
+ regen.disabled = false;
86
+ },
87
+ prefersReducedMotion ? 120 : 1200
88
+ );
71
89
  });
72
90
  }
73
91
  }