@anglefeint/astro-theme 0.1.39 → 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.39",
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 };
@@ -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';
@@ -335,25 +336,7 @@ const hasCommentsConfig = Boolean(
335
336
  {
336
337
  hasCommentsConfig && (
337
338
  <section class="prose ai-comments" aria-label={messages.blog.comments}>
338
- <script
339
- src="https://giscus.app/client.js"
340
- data-repo={comments.REPO}
341
- data-repo-id={comments.REPO_ID}
342
- data-category={comments.CATEGORY}
343
- data-category-id={comments.CATEGORY_ID}
344
- data-mapping={comments.MAPPING}
345
- data-term={comments.MAPPING === 'specific' ? normalizedCommentTerm : undefined}
346
- data-number={comments.MAPPING === 'number' ? normalizedCommentNumber : undefined}
347
- data-strict={comments.STRICT}
348
- data-reactions-enabled={comments.REACTIONS_ENABLED}
349
- data-emit-metadata={comments.EMIT_METADATA}
350
- data-input-position={comments.INPUT_POSITION}
351
- data-theme={comments.THEME}
352
- data-lang={comments.LANG || resolvedLocale}
353
- data-loading={comments.LOADING}
354
- crossorigin={comments.CROSSORIGIN}
355
- async
356
- />
339
+ <Giscus comments={comments} resolvedLocale={resolvedLocale} />
357
340
  </section>
358
341
  )
359
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
  }