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 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, "&lt;")
9
+ .replace(/>/g, "&gt;");
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) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossbook",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A lightweight static blog site generator for GitHub Pages",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -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
  <title>${page.config.blogName}: ${data.pageTitle}</title>
13
13
  <link rel="canonical" href="${page.url}" />
14
14
  ${page.alternateLanguageLinks()}
15
+ ${page.languagePreference()}
15
16
  ${page.openGraph("website")}
16
17
  </head>
17
18
  <body>
@@ -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",
@@ -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>
@@ -13,6 +13,7 @@
13
13
  <title>${page.config.blogName}: ${data.pageTitle}</title>
14
14
  <link rel="canonical" href="${page.url}" />
15
15
  ${page.alternateLanguageLinks()}
16
+ ${page.languagePreference()}
16
17
  ${page.openGraph("website")}
17
18
  </head>
18
19
  <body>