fossbook 0.2.1 → 0.2.3
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/init.js +1 -1
- package/lib/mod/marked.js +92 -4
- package/lib/mod/page_base.js +41 -0
- package/package.json +1 -1
- package/themes/archie/assets/styles/main.css +82 -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 +2 -1
- 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/init.js
CHANGED
|
@@ -128,7 +128,7 @@ function initProject(options = {}) {
|
|
|
128
128
|
// },
|
|
129
129
|
// Add localized blog information to fossbook.config.en.js,
|
|
130
130
|
// fossbook.config.ko.js, and so on. Menu labels can also be localized with
|
|
131
|
-
// homeLabel, allPostsLabel, aboutLabel, tagsLabel, and
|
|
131
|
+
// homeLabel, allPostsLabel, aboutLabel, tagsLabel, allTagsLabel, and transcriptLabel.
|
|
132
132
|
|
|
133
133
|
// Comment system (optional)
|
|
134
134
|
// comments: { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" },
|
package/lib/mod/marked.js
CHANGED
|
@@ -9,6 +9,52 @@ function escapeAttribute(value) {
|
|
|
9
9
|
.replace(/>/g, ">");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const panelStyleProperties = new Map([
|
|
13
|
+
["background", "--panel-background"],
|
|
14
|
+
["background-color", "--panel-background-color"],
|
|
15
|
+
["border", "--panel-border"],
|
|
16
|
+
["border-color", "--panel-border-color"],
|
|
17
|
+
["border-radius", "--panel-border-radius"],
|
|
18
|
+
["border-style", "--panel-border-style"],
|
|
19
|
+
["border-width", "--panel-border-width"],
|
|
20
|
+
["box-shadow", "--panel-box-shadow"],
|
|
21
|
+
["gap", "--panel-gap"],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function parsePanelStyle(source) {
|
|
25
|
+
const declarations = [];
|
|
26
|
+
const properties = new Set();
|
|
27
|
+
const declarationPattern = /([a-z][a-z-]*)\s*:\s*([^;]+)(?:;|$)/gy;
|
|
28
|
+
let offset = 0;
|
|
29
|
+
|
|
30
|
+
while (offset < source.length) {
|
|
31
|
+
while (/\s/.test(source[offset])) offset += 1;
|
|
32
|
+
if (offset === source.length) break;
|
|
33
|
+
|
|
34
|
+
declarationPattern.lastIndex = offset;
|
|
35
|
+
const match = declarationPattern.exec(source);
|
|
36
|
+
if (!match) throw new Error(`Invalid panel style near: ${source.slice(offset)}`);
|
|
37
|
+
|
|
38
|
+
const property = match[1];
|
|
39
|
+
const value = match[2].trim();
|
|
40
|
+
if (!panelStyleProperties.has(property)) {
|
|
41
|
+
throw new Error(`Unsupported panel style property: ${property}`);
|
|
42
|
+
}
|
|
43
|
+
if (properties.has(property)) {
|
|
44
|
+
throw new Error(`Duplicate panel style property: ${property}`);
|
|
45
|
+
}
|
|
46
|
+
if (!value || /[{}]|url\s*\(|expression\s*\(|@import|javascript\s*:/i.test(value)) {
|
|
47
|
+
throw new Error(`Invalid value for panel style property: ${property}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
properties.add(property);
|
|
51
|
+
declarations.push(`${panelStyleProperties.get(property)}: ${value};`);
|
|
52
|
+
offset = declarationPattern.lastIndex;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return declarations;
|
|
56
|
+
}
|
|
57
|
+
|
|
12
58
|
function parsePanelAttributes(source) {
|
|
13
59
|
const attributes = {};
|
|
14
60
|
const attributePattern = /([a-z][a-z-]*)="([^"]*)"/gy;
|
|
@@ -21,7 +67,7 @@ function parsePanelAttributes(source) {
|
|
|
21
67
|
attributePattern.lastIndex = offset;
|
|
22
68
|
const match = attributePattern.exec(source);
|
|
23
69
|
if (!match) throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
|
|
24
|
-
if (
|
|
70
|
+
if (!["columns", "label", "boxed", "style"].includes(match[1])) {
|
|
25
71
|
throw new Error(`Unsupported panels attribute: ${match[1]}`);
|
|
26
72
|
}
|
|
27
73
|
if (Object.hasOwn(attributes, match[1])) {
|
|
@@ -36,7 +82,20 @@ function parsePanelAttributes(source) {
|
|
|
36
82
|
throw new Error("Panel columns must be an integer from 1 to 6");
|
|
37
83
|
}
|
|
38
84
|
|
|
39
|
-
|
|
85
|
+
if (attributes.boxed !== undefined && attributes.boxed !== "true") {
|
|
86
|
+
throw new Error('Panel boxed must be "true"');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (attributes.style !== undefined && attributes.boxed !== "true") {
|
|
90
|
+
throw new Error('Panel style requires boxed="true"');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
columns,
|
|
95
|
+
label: attributes.label,
|
|
96
|
+
boxed: attributes.boxed === "true",
|
|
97
|
+
style: parsePanelStyle(attributes.style || ""),
|
|
98
|
+
};
|
|
40
99
|
}
|
|
41
100
|
|
|
42
101
|
const panelsExtension = {
|
|
@@ -55,6 +114,8 @@ const panelsExtension = {
|
|
|
55
114
|
raw: match[0],
|
|
56
115
|
columns: attributes.columns,
|
|
57
116
|
label: attributes.label,
|
|
117
|
+
boxed: attributes.boxed,
|
|
118
|
+
style: attributes.style,
|
|
58
119
|
tokens: this.lexer.blockTokens(match[2]),
|
|
59
120
|
};
|
|
60
121
|
},
|
|
@@ -62,7 +123,34 @@ const panelsExtension = {
|
|
|
62
123
|
const label = token.label
|
|
63
124
|
? ` role="group" aria-label="${escapeAttribute(token.label)}"`
|
|
64
125
|
: "";
|
|
65
|
-
|
|
126
|
+
const className = token.boxed ? "panel-group panel-group-boxed" : "panel-group";
|
|
127
|
+
const style = [`--panel-columns: ${token.columns};`, ...token.style].join(" ");
|
|
128
|
+
return `<div class="${className}" style="${escapeAttribute(style)}"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const comicBoxExtension = {
|
|
133
|
+
name: "comicBox",
|
|
134
|
+
level: "block",
|
|
135
|
+
start(source) {
|
|
136
|
+
return source.match(/^:::comic-box(?:[ \t]|$)/m)?.index;
|
|
137
|
+
},
|
|
138
|
+
tokenizer(source) {
|
|
139
|
+
const match = /^:::comic-box(?:[ \t]+label="([^"]*)")?[ \t]*\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
|
|
140
|
+
if (!match) return undefined;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
type: "comicBox",
|
|
144
|
+
raw: match[0],
|
|
145
|
+
label: match[1],
|
|
146
|
+
tokens: this.lexer.blockTokens(match[2]),
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
renderer(token) {
|
|
150
|
+
const label = token.label
|
|
151
|
+
? ` aria-label="${escapeAttribute(token.label)}"`
|
|
152
|
+
: "";
|
|
153
|
+
return `<section class="comic-box"${label}>\n${this.parser.parse(token.tokens)}</section>\n`;
|
|
66
154
|
},
|
|
67
155
|
};
|
|
68
156
|
|
|
@@ -179,7 +267,7 @@ const renderer = {
|
|
|
179
267
|
};
|
|
180
268
|
|
|
181
269
|
marked.use({
|
|
182
|
-
extensions: [panelsExtension],
|
|
270
|
+
extensions: [panelsExtension, comicBoxExtension],
|
|
183
271
|
renderer,
|
|
184
272
|
hooks: {
|
|
185
273
|
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
|
@@ -254,6 +254,81 @@
|
|
|
254
254
|
text-align: center;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
.panel-group-boxed {
|
|
258
|
+
align-items: stretch;
|
|
259
|
+
grid-auto-rows: 1fr;
|
|
260
|
+
gap: var(--panel-gap, 0.75rem);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.panel-group-boxed .image-container {
|
|
264
|
+
height: 100%;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.panel-group-boxed figure {
|
|
268
|
+
height: 100%;
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-direction: column;
|
|
271
|
+
border: var(--panel-border, 1px solid #575752);
|
|
272
|
+
border-radius: var(--panel-border-radius, 0);
|
|
273
|
+
background: var(--panel-background, #fff);
|
|
274
|
+
box-shadow: var(--panel-box-shadow, none);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.panel-group-boxed[style*="--panel-border-color:"] figure {
|
|
278
|
+
border-color: var(--panel-border-color);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.panel-group-boxed[style*="--panel-border-style:"] figure {
|
|
282
|
+
border-style: var(--panel-border-style);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.panel-group-boxed[style*="--panel-border-width:"] figure {
|
|
286
|
+
border-width: var(--panel-border-width);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.panel-group-boxed[style*="--panel-background-color:"] figure {
|
|
290
|
+
background-color: var(--panel-background-color);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.panel-group-boxed figcaption {
|
|
294
|
+
flex: 1;
|
|
295
|
+
margin-top: 0;
|
|
296
|
+
padding: 0.75rem;
|
|
297
|
+
font-size: 0.9rem;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.comic-box {
|
|
301
|
+
margin: 2rem 0;
|
|
302
|
+
padding: 1rem;
|
|
303
|
+
border: 2px solid #575752;
|
|
304
|
+
background: #fff;
|
|
305
|
+
box-shadow: 4px 4px 0 rgba(70, 70, 66, 0.14);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.comic-box > :first-child {
|
|
309
|
+
margin-top: 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.comic-box > :last-child {
|
|
313
|
+
margin-bottom: 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.comic-box .image-container,
|
|
317
|
+
.comic-box figure {
|
|
318
|
+
display: block;
|
|
319
|
+
width: 100%;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.comic-box figure {
|
|
323
|
+
padding: 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.comic-box .image-dialogue {
|
|
327
|
+
margin-bottom: 1rem;
|
|
328
|
+
padding-bottom: 1rem;
|
|
329
|
+
border-bottom: 1px solid #575752;
|
|
330
|
+
}
|
|
331
|
+
|
|
257
332
|
@media screen and (max-width: 599px) {
|
|
258
333
|
.panel-group {
|
|
259
334
|
grid-template-columns: 1fr;
|
|
@@ -262,6 +337,13 @@
|
|
|
262
337
|
.sized-image {
|
|
263
338
|
width: 100% !important;
|
|
264
339
|
}
|
|
340
|
+
|
|
341
|
+
.comic-box {
|
|
342
|
+
margin: 1.5rem 0;
|
|
343
|
+
padding: 0.75rem;
|
|
344
|
+
border-width: 1px;
|
|
345
|
+
box-shadow: 2px 2px 0 rgba(70, 70, 66, 0.14);
|
|
346
|
+
}
|
|
265
347
|
}
|
|
266
348
|
|
|
267
349
|
@media screen and (min-width: 600px) {
|
|
@@ -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" />
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<meta name="description" content="${page.description}" />
|
|
17
17
|
<link rel="canonical" href="${page.url}" />
|
|
18
18
|
${page.alternateLanguageLinks()}
|
|
19
|
+
${page.languagePreference()}
|
|
19
20
|
|
|
20
21
|
${page.openGraph(
|
|
21
22
|
"article",
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
</div>
|
|
58
59
|
${page.body.includes('class="blockquote-container image-dialogue"') ? `<div class="transcript-control">
|
|
59
60
|
<div class="transcript-toggle">
|
|
60
|
-
<label for="comic-transcript-toggle"
|
|
61
|
+
<label for="comic-transcript-toggle">${page.config.transcriptLabel || "Display dialogue text"}</label>
|
|
61
62
|
<input id="comic-transcript-toggle" type="checkbox" role="switch" aria-controls="comic-transcript" checked>
|
|
62
63
|
</div>
|
|
63
64
|
</div>` : ""}
|
|
@@ -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>
|