fossbook 0.2.0 → 0.2.2
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/README.md +6 -0
- package/lib/mod/marked.js +67 -1
- package/lib/mod/page_base.js +41 -0
- package/package.json +1 -1
- package/themes/archie/assets/styles/main.css +40 -0
- package/themes/archie/layouts/all_posts.html +1 -0
- package/themes/archie/layouts/home.html +1 -0
- package/themes/archie/layouts/page.html +1 -0
- package/themes/archie/layouts/post.html +1 -0
- package/themes/archie/layouts/tag.html +1 -0
- package/themes/archie/layouts/tag_list.html +1 -0
package/README.md
CHANGED
|
@@ -176,6 +176,12 @@ Fossbook generates a separate static page for each available language. The
|
|
|
176
176
|
browser loads only the selected language page; changing languages follows a
|
|
177
177
|
normal link and does not require JavaScript.
|
|
178
178
|
|
|
179
|
+
When a reader uses the language switcher, Fossbook remembers that choice in
|
|
180
|
+
the browser. A later visit to the default-language homepage redirects to the
|
|
181
|
+
remembered language's homepage. Direct links to posts and other pages are
|
|
182
|
+
never redirected, and language links continue to work when browser storage is
|
|
183
|
+
unavailable.
|
|
184
|
+
|
|
179
185
|
### Configuration
|
|
180
186
|
|
|
181
187
|
Add the languages supported by the site to `fossbook.config.js`:
|
package/lib/mod/marked.js
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
const marked = require("marked");
|
|
2
2
|
const hljs = require("highlight.js");
|
|
3
3
|
|
|
4
|
+
function escapeAttribute(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/"/g, """)
|
|
8
|
+
.replace(/</g, "<")
|
|
9
|
+
.replace(/>/g, ">");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parsePanelAttributes(source) {
|
|
13
|
+
const attributes = {};
|
|
14
|
+
const attributePattern = /([a-z][a-z-]*)="([^"]*)"/gy;
|
|
15
|
+
let offset = 0;
|
|
16
|
+
|
|
17
|
+
while (offset < source.length) {
|
|
18
|
+
while (source[offset] === " " || source[offset] === "\t") offset += 1;
|
|
19
|
+
if (offset === source.length) break;
|
|
20
|
+
|
|
21
|
+
attributePattern.lastIndex = offset;
|
|
22
|
+
const match = attributePattern.exec(source);
|
|
23
|
+
if (!match) throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
|
|
24
|
+
if (match[1] !== "columns" && match[1] !== "label") {
|
|
25
|
+
throw new Error(`Unsupported panels attribute: ${match[1]}`);
|
|
26
|
+
}
|
|
27
|
+
if (Object.hasOwn(attributes, match[1])) {
|
|
28
|
+
throw new Error(`Duplicate panels attribute: ${match[1]}`);
|
|
29
|
+
}
|
|
30
|
+
attributes[match[1]] = match[2];
|
|
31
|
+
offset = attributePattern.lastIndex;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const columns = attributes.columns === undefined ? 2 : Number(attributes.columns);
|
|
35
|
+
if (!Number.isInteger(columns) || columns < 1 || columns > 6) {
|
|
36
|
+
throw new Error("Panel columns must be an integer from 1 to 6");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { columns, label: attributes.label };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const panelsExtension = {
|
|
43
|
+
name: "panels",
|
|
44
|
+
level: "block",
|
|
45
|
+
start(source) {
|
|
46
|
+
return source.match(/^:::panels(?:[ \t]|$)/m)?.index;
|
|
47
|
+
},
|
|
48
|
+
tokenizer(source) {
|
|
49
|
+
const match = /^:::panels([^\n]*)\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
|
|
50
|
+
if (!match) return undefined;
|
|
51
|
+
|
|
52
|
+
const attributes = parsePanelAttributes(match[1].trim());
|
|
53
|
+
return {
|
|
54
|
+
type: "panels",
|
|
55
|
+
raw: match[0],
|
|
56
|
+
columns: attributes.columns,
|
|
57
|
+
label: attributes.label,
|
|
58
|
+
tokens: this.lexer.blockTokens(match[2]),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
renderer(token) {
|
|
62
|
+
const label = token.label
|
|
63
|
+
? ` role="group" aria-label="${escapeAttribute(token.label)}"`
|
|
64
|
+
: "";
|
|
65
|
+
return `<div class="panel-group" style="--panel-columns: ${token.columns};"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
4
69
|
marked.setOptions({
|
|
5
70
|
renderer: new marked.Renderer(),
|
|
6
71
|
pedantic: false,
|
|
@@ -66,7 +131,7 @@ const renderer = {
|
|
|
66
131
|
// Construct the image tag with optional size and title
|
|
67
132
|
let imageTag = `<img src="${href}" alt="${text}"`;
|
|
68
133
|
if (size) {
|
|
69
|
-
imageTag += ` style="width: ${size};"`;
|
|
134
|
+
imageTag += ` class="sized-image" style="width: ${size};"`;
|
|
70
135
|
}
|
|
71
136
|
imageTag += ">";
|
|
72
137
|
|
|
@@ -114,6 +179,7 @@ const renderer = {
|
|
|
114
179
|
};
|
|
115
180
|
|
|
116
181
|
marked.use({
|
|
182
|
+
extensions: [panelsExtension],
|
|
117
183
|
renderer,
|
|
118
184
|
hooks: {
|
|
119
185
|
postprocess(html) {
|
package/lib/mod/page_base.js
CHANGED
|
@@ -48,6 +48,47 @@ module.exports = class PageBase {
|
|
|
48
48
|
return `<nav class="language-switcher" aria-label="Languages">${links.join("")}</nav>`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
languagePreference(redirectFromDefaultHome = false) {
|
|
52
|
+
if (this.translations.length < 2) return "";
|
|
53
|
+
|
|
54
|
+
const supportedLanguages = this.translations.map((translation) => translation.language);
|
|
55
|
+
const languagePaths = Object.fromEntries(
|
|
56
|
+
this.translations.map((translation) => [
|
|
57
|
+
translation.language,
|
|
58
|
+
translation.path || translation.url,
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
const shouldRedirect =
|
|
62
|
+
redirectFromDefaultHome && this.config.language === this.config.defaultLanguage;
|
|
63
|
+
|
|
64
|
+
return `<script>
|
|
65
|
+
(() => {
|
|
66
|
+
const storageKey = "fossbook-language";
|
|
67
|
+
const supportedLanguages = ${JSON.stringify(supportedLanguages).replace(/</g, "\\u003c")};
|
|
68
|
+
const languagePaths = ${JSON.stringify(languagePaths).replace(/</g, "\\u003c")};
|
|
69
|
+
try {
|
|
70
|
+
${shouldRedirect ? `const preferredLanguage = localStorage.getItem(storageKey);
|
|
71
|
+
if (preferredLanguage && preferredLanguage !== ${JSON.stringify(this.config.language)} && supportedLanguages.includes(preferredLanguage)) {
|
|
72
|
+
location.replace(languagePaths[preferredLanguage]);
|
|
73
|
+
return;
|
|
74
|
+
}` : ""}
|
|
75
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
76
|
+
document.querySelectorAll('.language-switcher a[hreflang]').forEach((link) => {
|
|
77
|
+
link.addEventListener("click", () => {
|
|
78
|
+
const language = link.getAttribute("hreflang");
|
|
79
|
+
if (supportedLanguages.includes(language)) {
|
|
80
|
+
try {
|
|
81
|
+
localStorage.setItem(storageKey, language);
|
|
82
|
+
} catch (error) {}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {}
|
|
88
|
+
})();
|
|
89
|
+
</script>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
51
92
|
googleAnalytics(trackingId) {
|
|
52
93
|
return `<script async src="https://www.googletagmanager.com/gtag/js?id=${trackingId}"></script>
|
|
53
94
|
<script>
|
package/package.json
CHANGED
|
@@ -223,6 +223,46 @@
|
|
|
223
223
|
figure img {
|
|
224
224
|
max-height: 500px;
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
.panel-group {
|
|
228
|
+
display: grid;
|
|
229
|
+
grid-template-columns: repeat(var(--panel-columns), minmax(0, 1fr));
|
|
230
|
+
gap: 1.5rem;
|
|
231
|
+
align-items: start;
|
|
232
|
+
margin: 1.5rem 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.panel-group .image-container,
|
|
236
|
+
.panel-group figure {
|
|
237
|
+
min-width: 0;
|
|
238
|
+
width: 100%;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.panel-group figure {
|
|
242
|
+
display: block;
|
|
243
|
+
padding: 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.panel-group img {
|
|
247
|
+
display: block;
|
|
248
|
+
height: auto;
|
|
249
|
+
width: 100%;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.panel-group figcaption {
|
|
253
|
+
margin-top: 0.75rem;
|
|
254
|
+
text-align: center;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media screen and (max-width: 599px) {
|
|
258
|
+
.panel-group {
|
|
259
|
+
grid-template-columns: 1fr;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.sized-image {
|
|
263
|
+
width: 100% !important;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
226
266
|
|
|
227
267
|
@media screen and (min-width: 600px) {
|
|
228
268
|
figure {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<meta name="description" content="${page.config.blogDescription}" />
|
|
13
13
|
<link rel="canonical" href="${page.url}" />
|
|
14
14
|
${page.alternateLanguageLinks()}
|
|
15
|
+
${page.languagePreference(data.prev === null)}
|
|
15
16
|
${page.openGraph("website")}
|
|
16
17
|
<!-- Twitter Card -->
|
|
17
18
|
${page.twitterCard("summary")}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<meta name="description" content="${page.config.blogDescription}" />
|
|
14
14
|
<link rel="canonical" href="${page.url}" />
|
|
15
15
|
${page.alternateLanguageLinks()}
|
|
16
|
+
${page.languagePreference()}
|
|
16
17
|
${page.openGraph("website")}
|
|
17
18
|
|
|
18
19
|
<meta name="twitter:card" content="summary_large_image" />
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<!-- Google tag (gtag.js) -->
|
|
11
11
|
${page.config.googleAnalyticsID ? page.googleAnalytics(page.config.googleAnalyticsID) : ""}
|
|
12
12
|
<title>${page.config.blogName}: Entries tagged - ${data.tag}</title>
|
|
13
|
+
${page.languagePreference()}
|
|
13
14
|
${page.openGraph("website")}
|
|
14
15
|
</head>
|
|
15
16
|
<body>
|