@rathnasgala/theme 0.0.1 → 0.0.5
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 -2
- package/payload/.gala/managed-files.json +18 -15
- package/payload/eleventy.config.js +10 -2
- package/payload/lib/performance-budget.js +39 -0
- package/payload/lib/provider-fixtures/share-intents.v1.json +13 -0
- package/payload/lib/render-markdown.js +84 -5
- package/payload/lib/seo.js +11 -1
- package/payload/lib/share-targets.js +63 -0
- package/payload/lib/site-config.js +26 -1
- package/payload/package-lock.json +613 -1
- package/payload/package.json +5 -2
- package/payload/src/_includes/components/ui.njk +20 -1
- package/payload/src/_includes/layouts/base.njk +56 -5
- package/payload/src/_includes/layouts/post.njk +4 -3
- package/payload/src/assets/interactions.js +384 -0
- package/payload/src/assets/search.js +2 -4
- package/payload/src/assets/theme-mode.js +3 -1
- package/payload/src/assets/theme.css +60 -0
- package/payload/src/posts.11ty.js +26 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathnasgala/theme",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Managed Gala static-site theme payload",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
"provenance": true
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
18
|
+
"node": ">=20"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.5",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
7
|
"editorial"
|
|
8
8
|
],
|
|
@@ -11,33 +11,36 @@
|
|
|
11
11
|
"files": {
|
|
12
12
|
".gitignore": "b5fa24e10155097a0d372926fd2e59e6072e7cc2cb6399bbfb4beb8b33ea315b",
|
|
13
13
|
".gala/publish.yml.template": "73080241d89bd09964b797a8bcc8e18471412e11bc873efad76305b60f497d08",
|
|
14
|
-
"eleventy.config.js": "
|
|
15
|
-
"package.json": "
|
|
16
|
-
"package-lock.json": "
|
|
14
|
+
"eleventy.config.js": "24f25bd69108f17c80d85807f1bbf6851a44b77dfdddc8d8e234b85462cb2e04",
|
|
15
|
+
"package.json": "cccea6fc4edf4b4b653bd5cfc0901ab20dd92ac8975408fb01ae3670005da641",
|
|
16
|
+
"package-lock.json": "c97a6a2f794aef083a39a8d2628eccc4a3c484e1c6225899b77ec1fb7c466d9e",
|
|
17
17
|
"lib/build-manifest.js": "88949f0112d683ca809bb67ca969e15634792423559a67bc79a534cd807e5535",
|
|
18
|
-
"lib/site-config.js": "
|
|
18
|
+
"lib/site-config.js": "83aa8ea678e06ae0e85096c841a2d6084ee23c7f236191ea426bbeda99567d16",
|
|
19
|
+
"lib/performance-budget.js": "124c43494c092a956accec46d1463ea063ed73363d0db01f24aaea987459d683",
|
|
19
20
|
"lib/engagement-snapshot.js": "298c11b1ab135a3a98a4e31b6320ab60dfd7fda6a9d9053df37d86282a5a6687",
|
|
20
21
|
"lib/publication-state.js": "1b59c29a18bbb1193bddea898dac3a4fb832c7bc4751263be086f3d64392dea0",
|
|
21
|
-
"lib/render-markdown.js": "
|
|
22
|
-
"lib/seo.js": "
|
|
22
|
+
"lib/render-markdown.js": "c7dac7e3599af445830825e76e285166e85835163caa8bef2c3a970a307ac750",
|
|
23
|
+
"lib/seo.js": "107de8804931945b8e73894877fc394d6b535a9d0d969a4ba5b68f2e136e0a17",
|
|
24
|
+
"lib/share-targets.js": "5820466526ab2dcd2cd1689c163117c34df0a675dfa92e28e853f690423299b3",
|
|
25
|
+
"lib/provider-fixtures/share-intents.v1.json": "2535cee63962f38691cb7b8c47d2414bf9cacbcee5cadba0bec0172e7b3961e4",
|
|
23
26
|
"src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
|
|
24
|
-
"src/_includes/layouts/base.njk": "
|
|
25
|
-
"src/_includes/layouts/post.njk": "
|
|
26
|
-
"src/_includes/components/ui.njk": "
|
|
27
|
+
"src/_includes/layouts/base.njk": "47ad79e1ce9520caacb4e564f3257335efc8889c242b2064ce2ed27c98d2cfcc",
|
|
28
|
+
"src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
|
|
29
|
+
"src/_includes/components/ui.njk": "3a0eeb0f7b8f0ea46ed39d4c53b810a33b997766cbae540ef063c8ecdf641809",
|
|
27
30
|
"src/_data/buildManifest.js": "628163305984e0d3537e27b0d4050f5ba5ead12a834878266d2ce8004ffe1b1e",
|
|
28
31
|
"src/_data/engagementSnapshot.js": "9217dacebfaf008f7a0ad67ff4e8d1fa8fba609a9dc7e3430c78c5b32024da0e",
|
|
29
32
|
"src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
|
|
30
33
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
31
34
|
"src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
|
|
32
|
-
"src/assets/theme-mode.js": "
|
|
33
|
-
"src/assets/interactions.js": "
|
|
35
|
+
"src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
|
|
36
|
+
"src/assets/interactions.js": "611e54376f03d9cd8abd55ef4fcbebe917a174c673dd15fcb3c4073cc6e79836",
|
|
34
37
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
35
|
-
"src/assets/search.js": "
|
|
36
|
-
"src/assets/theme.css": "
|
|
38
|
+
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
39
|
+
"src/assets/theme.css": "8cad6e82e0017e7b62a5355f0386d854fb62e774c520b3ce25aaea5b19b4fa61",
|
|
37
40
|
"src/index.njk": "5c313f0fcdad3ddbfb79376bc9671b3f37539ef70c14bdbbd26d6e2e6de250e8",
|
|
38
41
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
39
42
|
"src/languages.11ty.js": "399559199c65570d7d5efd88e20254838e316f81c06d7b28ec34990e963db915",
|
|
40
|
-
"src/posts.11ty.js": "
|
|
43
|
+
"src/posts.11ty.js": "2f9d738075eab68961d7c8fe9286b61f12c2c677f7dad9d4858927ef51b66ff8",
|
|
41
44
|
"src/redirects.11ty.js": "3c163246a59f5364861db6a6f05ae88afc938a5379d86cf42776d8d49b0031f3",
|
|
42
45
|
"src/search-index.11ty.js": "700dc2a2be0ef20de3a828a4612fcf7a082691eade29c6e1d75f09c24b6c6024",
|
|
43
46
|
"src/search.njk": "63f2e8aeebb63c90da2e375eece639ce096cad1eff7f28628cafc46faf7ceee6",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { markdownLibrary } from './lib/render-markdown.js';
|
|
2
2
|
import { readBuildManifest } from './lib/build-manifest.js';
|
|
3
|
+
import { loadSiteConfiguration } from './lib/site-config.js';
|
|
4
|
+
import { enforcePerformanceBudgets } from './lib/performance-budget.js';
|
|
3
5
|
import { lstat, realpath } from 'node:fs/promises';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
|
|
@@ -20,7 +22,7 @@ async function verifiedMediaSource(postSource, mediaSource) {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export default async function (eleventyConfig) {
|
|
23
|
-
const manifest = await readBuildManifest();
|
|
25
|
+
const [manifest, site] = await Promise.all([readBuildManifest(), loadSiteConfiguration()]);
|
|
24
26
|
eleventyConfig.setLibrary('md', markdownLibrary);
|
|
25
27
|
eleventyConfig.addPassthroughCopy({ static: '/' });
|
|
26
28
|
eleventyConfig.addPassthroughCopy({ 'src/assets': 'assets' });
|
|
@@ -34,6 +36,12 @@ export default async function (eleventyConfig) {
|
|
|
34
36
|
eleventyConfig.addCollection('posts', (collectionApi) =>
|
|
35
37
|
collectionApi.getAll().filter((item) => item.data.post?.publicationState === 'published')
|
|
36
38
|
);
|
|
39
|
+
eleventyConfig.on('eleventy.after', async () => {
|
|
40
|
+
await enforcePerformanceBudgets({
|
|
41
|
+
outputDirectory: path.resolve('_site'),
|
|
42
|
+
budgets: site.performance.budgets
|
|
43
|
+
});
|
|
44
|
+
});
|
|
37
45
|
|
|
38
46
|
return {
|
|
39
47
|
dir: {
|
|
@@ -42,6 +50,6 @@ export default async function (eleventyConfig) {
|
|
|
42
50
|
includes: '_includes',
|
|
43
51
|
data: '_data'
|
|
44
52
|
},
|
|
45
|
-
pathPrefix:
|
|
53
|
+
pathPrefix: site.hosting.pathPrefix === '' ? '/' : site.hosting.pathPrefix
|
|
46
54
|
};
|
|
47
55
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function filesBelow(directory) {
|
|
5
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
6
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
7
|
+
const file = path.join(directory, entry.name);
|
|
8
|
+
return entry.isDirectory() ? filesBelow(file) : entry.isFile() ? [file] : [];
|
|
9
|
+
}));
|
|
10
|
+
return nested.flat();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function totalBytes(files) {
|
|
14
|
+
return (await Promise.all(files.map(async (file) => (await stat(file)).size)))
|
|
15
|
+
.reduce((total, size) => total + size, 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertWithin(actual, maximum, label) {
|
|
19
|
+
if (actual > maximum) {
|
|
20
|
+
throw new Error(`${label} performance budget exceeded: ${actual} bytes > ${maximum} bytes`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function enforcePerformanceBudgets({ outputDirectory, budgets }) {
|
|
25
|
+
const output = path.resolve(outputDirectory);
|
|
26
|
+
const files = await filesBelow(output);
|
|
27
|
+
const managedAssets = files.filter((file) => path.dirname(file) === path.join(output, 'assets'));
|
|
28
|
+
const javascriptBytes = await totalBytes(managedAssets.filter((file) => file.endsWith('.js')));
|
|
29
|
+
const cssBytes = await totalBytes(managedAssets.filter((file) => file.endsWith('.css')));
|
|
30
|
+
assertWithin(javascriptBytes, budgets.managedJavaScriptBytes, 'Managed JavaScript');
|
|
31
|
+
assertWithin(cssBytes, budgets.managedCssBytes, 'Managed CSS');
|
|
32
|
+
|
|
33
|
+
for (const file of files.filter((candidate) => candidate.endsWith('.html'))) {
|
|
34
|
+
const bytes = (await stat(file)).size;
|
|
35
|
+
assertWithin(bytes, budgets.ordinaryHtmlBytes, path.relative(output, file));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return Object.freeze({ javascriptBytes, cssBytes });
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"providers": {
|
|
4
|
+
"bluesky": { "source": "https://docs.bsky.app/docs/advanced-guides/intent-links", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://bsky.app/intent/compose?text={text}" },
|
|
5
|
+
"x": { "source": "https://developer.x.com/en/docs/twitter-for-websites/tweet-button/guides/web-intent", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://twitter.com/intent/tweet?text={text}&url={url}" },
|
|
6
|
+
"linkedin": { "source": "https://www.linkedin.com/help/linkedin/answer/a522735", "status": "blocked-public-intent-contract-unverified" },
|
|
7
|
+
"reddit": { "source": "https://www.reddit.com/dev/api/#POST_api_submit", "status": "blocked-public-intent-contract-unverified" },
|
|
8
|
+
"hacker-news": { "source": "https://news.ycombinator.com/bookmarklet.html", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://news.ycombinator.com/submitlink?u={url}&t={title}" },
|
|
9
|
+
"whatsapp": { "source": "https://faq.whatsapp.com/5913398998672934", "status": "blocked-public-intent-contract-unverified" },
|
|
10
|
+
"email": { "source": "https://www.rfc-editor.org/rfc/rfc6068", "verifiedOn": "2026-08-13", "status": "verified", "template": "mailto:?subject={title}&body={text}" },
|
|
11
|
+
"mastodon": { "source": "https://docs.joinmastodon.org/user/external/#share", "verifiedOn": "2026-08-13", "status": "verified", "template": "https://{instance}/share?text={text}", "requiresInstance": true }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import footnote from 'markdown-it-footnote';
|
|
3
3
|
import sanitizeHtml from 'sanitize-html';
|
|
4
|
+
import { createHighlighter } from 'shiki';
|
|
5
|
+
import GithubSlugger from 'github-slugger';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const SHIKI_THEMES = Object.freeze({ light: 'github-light', dark: 'github-dark' });
|
|
9
|
+
const SHIKI_LANGUAGES = Object.freeze([
|
|
10
|
+
'bash', 'css', 'html', 'java', 'javascript', 'json', 'jsx', 'markdown',
|
|
11
|
+
'shellscript', 'sql', 'typescript', 'tsx', 'yaml'
|
|
12
|
+
]);
|
|
13
|
+
const highlighter = await createHighlighter({
|
|
14
|
+
themes: Object.values(SHIKI_THEMES),
|
|
15
|
+
langs: SHIKI_LANGUAGES
|
|
16
|
+
});
|
|
4
17
|
|
|
5
18
|
const ADMONITION_TYPES = new Set(['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION']);
|
|
6
19
|
|
|
@@ -38,9 +51,44 @@ function admonitions(markdown) {
|
|
|
38
51
|
});
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
function headingAnchors(markdown) {
|
|
55
|
+
markdown.core.ruler.push('gala_heading_anchors', (state) => {
|
|
56
|
+
const slugger = new GithubSlugger();
|
|
57
|
+
const toc = [];
|
|
58
|
+
for (let index = 0; index < state.tokens.length - 1; index += 1) {
|
|
59
|
+
const open = state.tokens[index];
|
|
60
|
+
if (open.type !== 'heading_open') continue;
|
|
61
|
+
const inline = state.tokens[index + 1];
|
|
62
|
+
const id = slugger.slug(inline.content);
|
|
63
|
+
open.attrSet('id', id);
|
|
64
|
+
if (open.tag === 'h2' || open.tag === 'h3') toc.push({ id, text: inline.content });
|
|
65
|
+
}
|
|
66
|
+
state.env.galaToc = toc.length >= 3 ? toc : [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
const markdown = new MarkdownIt({ html: true, linkify: true, typographer: false })
|
|
42
71
|
.use(footnote)
|
|
43
|
-
.use(admonitions)
|
|
72
|
+
.use(admonitions)
|
|
73
|
+
.use(headingAnchors);
|
|
74
|
+
|
|
75
|
+
const defaultFence = markdown.renderer.rules.fence.bind(markdown.renderer.rules);
|
|
76
|
+
markdown.renderer.rules.fence = (tokens, index, options, environment, self) => {
|
|
77
|
+
const language = tokens[index].info.trim().split(/\s+/, 1)[0].toLowerCase();
|
|
78
|
+
if (!SHIKI_LANGUAGES.includes(language)) {
|
|
79
|
+
return defaultFence(tokens, index, options, environment, self);
|
|
80
|
+
}
|
|
81
|
+
const highlights = environment.galaHighlights ??= [];
|
|
82
|
+
const token = randomBytes(18).toString('hex');
|
|
83
|
+
highlights.push(highlighter.codeToHtml(tokens[index].content, {
|
|
84
|
+
lang: language,
|
|
85
|
+
themes: SHIKI_THEMES,
|
|
86
|
+
defaultColor: false
|
|
87
|
+
}));
|
|
88
|
+
const tokensByIndex = environment.galaHighlightTokens ??= [];
|
|
89
|
+
tokensByIndex.push(token);
|
|
90
|
+
return `<div class="gala-highlight-placeholder" data-highlight-token="${token}"></div>`;
|
|
91
|
+
};
|
|
44
92
|
|
|
45
93
|
const sanitizeOptions = {
|
|
46
94
|
allowedTags: [
|
|
@@ -53,7 +101,13 @@ const sanitizeOptions = {
|
|
|
53
101
|
a: ['href', 'title', 'rel'],
|
|
54
102
|
aside: ['class', 'role'],
|
|
55
103
|
code: ['class'],
|
|
56
|
-
div: ['class', 'id'],
|
|
104
|
+
div: ['class', 'id', 'data-highlight-token'],
|
|
105
|
+
h1: ['id'],
|
|
106
|
+
h2: ['id'],
|
|
107
|
+
h3: ['id'],
|
|
108
|
+
h4: ['id'],
|
|
109
|
+
h5: ['id'],
|
|
110
|
+
h6: ['id'],
|
|
57
111
|
img: ['src', 'alt', 'title', 'width', 'height', 'loading', 'decoding'],
|
|
58
112
|
li: ['id'],
|
|
59
113
|
ol: ['class'],
|
|
@@ -67,6 +121,7 @@ const sanitizeOptions = {
|
|
|
67
121
|
'admonition', 'admonition-note', 'admonition-tip', 'admonition-important',
|
|
68
122
|
'admonition-warning', 'admonition-caution'
|
|
69
123
|
],
|
|
124
|
+
div: ['gala-highlight-placeholder'],
|
|
70
125
|
code: [/^language-[a-z0-9_-]+$/],
|
|
71
126
|
ol: ['footnotes-list'],
|
|
72
127
|
span: ['footnote-backref'],
|
|
@@ -81,16 +136,40 @@ const sanitizeOptions = {
|
|
|
81
136
|
enforceHtmlBoundary: true
|
|
82
137
|
};
|
|
83
138
|
|
|
139
|
+
function render(source, inline, suppliedEnvironment) {
|
|
140
|
+
const environment = suppliedEnvironment ?? {};
|
|
141
|
+
environment.galaHighlights = [];
|
|
142
|
+
environment.galaHighlightTokens = [];
|
|
143
|
+
const unsafe = inline
|
|
144
|
+
? markdown.renderInline(source, environment)
|
|
145
|
+
: markdown.render(source, environment);
|
|
146
|
+
let safe = sanitizeHtml(unsafe, sanitizeOptions);
|
|
147
|
+
for (let index = 0; index < environment.galaHighlights.length; index += 1) {
|
|
148
|
+
safe = safe.replace(
|
|
149
|
+
`<div class="gala-highlight-placeholder" data-highlight-token="${environment.galaHighlightTokens[index]}"></div>`,
|
|
150
|
+
environment.galaHighlights[index]
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return safe;
|
|
154
|
+
}
|
|
155
|
+
|
|
84
156
|
export function renderMarkdown(source) {
|
|
85
157
|
if (typeof source !== 'string') throw new TypeError('Markdown source must be a string');
|
|
86
|
-
return
|
|
158
|
+
return render(source, false);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function renderMarkdownDocument(source) {
|
|
162
|
+
if (typeof source !== 'string') throw new TypeError('Markdown source must be a string');
|
|
163
|
+
const environment = {};
|
|
164
|
+
const html = render(source, false, environment);
|
|
165
|
+
return Object.freeze({ html, tableOfContents: Object.freeze(environment.galaToc ?? []) });
|
|
87
166
|
}
|
|
88
167
|
|
|
89
168
|
export const markdownLibrary = Object.assign(Object.create(markdown), {
|
|
90
169
|
render(source, environment) {
|
|
91
|
-
return
|
|
170
|
+
return render(source, false, environment);
|
|
92
171
|
},
|
|
93
172
|
renderInline(source, environment) {
|
|
94
|
-
return
|
|
173
|
+
return render(source, true, environment);
|
|
95
174
|
}
|
|
96
175
|
});
|
package/payload/lib/seo.js
CHANGED
|
@@ -84,9 +84,19 @@ function urlPath(value, field) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function siteUrl({ canonicalBaseUrl, pathPrefix = '/', relativePath = '/' }) {
|
|
87
|
+
let suppliedBase;
|
|
88
|
+
try {
|
|
89
|
+
suppliedBase = new URL(canonicalBaseUrl);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new TypeError('canonicalBaseUrl must be an absolute URL');
|
|
92
|
+
}
|
|
87
93
|
const base = absoluteHttpsUrl(canonicalBaseUrl, 'canonicalBaseUrl');
|
|
94
|
+
if (base.pathname !== '/' || suppliedBase.search !== '' || suppliedBase.hash !== '') {
|
|
95
|
+
throw new TypeError('canonicalBaseUrl must be an origin only; put the URL path in pathPrefix');
|
|
96
|
+
}
|
|
97
|
+
const normalizedPrefix = pathPrefix === '' ? '/' : pathPrefix;
|
|
88
98
|
const parts = [
|
|
89
|
-
...urlPath(
|
|
99
|
+
...urlPath(normalizedPrefix, 'pathPrefix'),
|
|
90
100
|
...urlPath(relativePath, 'relativePath')
|
|
91
101
|
].map(encodeURIComponent);
|
|
92
102
|
base.pathname = parts.length === 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const fixture = JSON.parse(readFileSync(
|
|
4
|
+
new URL('./provider-fixtures/share-intents.v1.json', import.meta.url),
|
|
5
|
+
'utf8'
|
|
6
|
+
));
|
|
7
|
+
|
|
8
|
+
const LABELS = Object.freeze({
|
|
9
|
+
bluesky: 'Bluesky',
|
|
10
|
+
x: 'X',
|
|
11
|
+
'hacker-news': 'Hacker News',
|
|
12
|
+
email: 'Email',
|
|
13
|
+
mastodon: 'Mastodon'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function resolveShareTargets({ configured, title, canonicalUrl, socialProfiles = {} }) {
|
|
17
|
+
if (!Array.isArray(configured)) throw new TypeError('sharing.targets must be an array');
|
|
18
|
+
if (typeof title !== 'string' || title.trim() === '') throw new TypeError('share title is required');
|
|
19
|
+
const canonical = requireHttpsUrl(canonicalUrl, 'canonical URL');
|
|
20
|
+
return configured.map((provider) => resolveProvider(provider, title, canonical, socialProfiles));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveProvider(provider, title, canonical, socialProfiles) {
|
|
24
|
+
const contract = fixture.providers?.[provider];
|
|
25
|
+
if (contract?.status !== 'verified' || typeof contract.template !== 'string') {
|
|
26
|
+
throw new TypeError(`Share target ${provider} has no verified public intent contract`);
|
|
27
|
+
}
|
|
28
|
+
let instance = '';
|
|
29
|
+
if (contract.requiresInstance) {
|
|
30
|
+
const profile = requireHttpsUrl(socialProfiles.mastodon, 'sharing.socialProfiles.mastodon');
|
|
31
|
+
instance = profile.host;
|
|
32
|
+
}
|
|
33
|
+
const text = provider === 'bluesky' || provider === 'mastodon'
|
|
34
|
+
? `${title}\n${canonical.href}`
|
|
35
|
+
: provider === 'email' ? canonical.href : title;
|
|
36
|
+
const replacements = {
|
|
37
|
+
text: encodeURIComponent(text),
|
|
38
|
+
title: encodeURIComponent(title),
|
|
39
|
+
url: encodeURIComponent(canonical.href),
|
|
40
|
+
instance
|
|
41
|
+
};
|
|
42
|
+
const rendered = contract.template.replace(/\{(text|title|url|instance)\}/g,
|
|
43
|
+
(placeholder, name) => replacements[name]);
|
|
44
|
+
if (/\{[^}]+\}/.test(rendered)) throw new TypeError(`Share target ${provider} fixture is incomplete`);
|
|
45
|
+
const destination = new URL(rendered);
|
|
46
|
+
if (!['https:', 'mailto:'].includes(destination.protocol)) {
|
|
47
|
+
throw new TypeError(`Share target ${provider} produced an unsafe URL`);
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({ provider, label: LABELS[provider], url: destination.href });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireHttpsUrl(value, field) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(value);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new TypeError(`${field} must be an absolute HTTPS URL`);
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol !== 'https:' || parsed.username !== '' || parsed.password !== '') {
|
|
60
|
+
throw new TypeError(`${field} must be an absolute credential-free HTTPS URL`);
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
@@ -2,6 +2,29 @@ import { lstat, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
|
|
5
|
+
const BUDGET_KEYS = Object.freeze([
|
|
6
|
+
'managedJavaScriptBytes',
|
|
7
|
+
'managedCssBytes',
|
|
8
|
+
'ordinaryHtmlBytes'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function validatePerformanceBudgets(value) {
|
|
12
|
+
if (value == null || Array.isArray(value) || typeof value !== 'object') {
|
|
13
|
+
throw new TypeError('performance.budgets must be a mapping');
|
|
14
|
+
}
|
|
15
|
+
const keys = Object.keys(value);
|
|
16
|
+
const unknown = keys.filter((key) => !BUDGET_KEYS.includes(key));
|
|
17
|
+
if (unknown.length > 0) {
|
|
18
|
+
throw new TypeError(`Unsupported performance budget: ${unknown.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
for (const key of BUDGET_KEYS) {
|
|
21
|
+
if (!Number.isSafeInteger(value[key]) || value[key] <= 0) {
|
|
22
|
+
throw new TypeError(`performance.budgets.${key} must be a positive integer byte count`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze(Object.fromEntries(BUDGET_KEYS.map((key) => [key, value[key]])));
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
export async function loadSiteConfiguration({
|
|
6
29
|
root = process.cwd(),
|
|
7
30
|
configPath = process.env.GALA_CONFIG_PATH ?? 'site.config.yml'
|
|
@@ -19,5 +42,7 @@ export async function loadSiteConfiguration({
|
|
|
19
42
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
20
43
|
throw new TypeError('site configuration must be a regular file');
|
|
21
44
|
}
|
|
22
|
-
|
|
45
|
+
const config = parse(await readFile(file, 'utf8'));
|
|
46
|
+
validatePerformanceBudgets(config?.performance?.budgets);
|
|
47
|
+
return config;
|
|
23
48
|
}
|