fossbook 0.2.2 → 0.2.4

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/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 allTagsLabel.
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,81 @@ function escapeAttribute(value) {
9
9
  .replace(/>/g, ">");
10
10
  }
11
11
 
12
+ function matchFencedContainer(source, name) {
13
+ const opening = new RegExp(`^(:{3,})${name}(?=[ \\t]|\\n)([^\\n]*)\\n`).exec(source);
14
+ if (!opening) return undefined;
15
+
16
+ const contentStart = opening[0].length;
17
+ const closing = new RegExp(`^${opening[1]}[ \\t]*(?:\\n|$)`, "m")
18
+ .exec(source.slice(contentStart));
19
+ if (!closing) return undefined;
20
+
21
+ const contentEnd = contentStart + closing.index;
22
+ const rawEnd = contentEnd + closing[0].length;
23
+ return {
24
+ attributes: opening[2].trim(),
25
+ content: source.slice(contentStart, contentEnd),
26
+ raw: source.slice(0, rawEnd),
27
+ };
28
+ }
29
+
30
+ const panelStyleProperties = new Map([
31
+ ["background", "--panel-background"],
32
+ ["background-color", "--panel-background-color"],
33
+ ["border", "--panel-border"],
34
+ ["border-color", "--panel-border-color"],
35
+ ["border-radius", "--panel-border-radius"],
36
+ ["border-style", "--panel-border-style"],
37
+ ["border-width", "--panel-border-width"],
38
+ ["box-shadow", "--panel-box-shadow"],
39
+ ["gap", "--panel-gap"],
40
+ ]);
41
+
42
+ const comicPanelStyleProperties = new Map([
43
+ ["background", "--comic-panel-background"],
44
+ ["background-color", "--comic-panel-background-color"],
45
+ ["border", "--comic-panel-border"],
46
+ ["border-color", "--comic-panel-border-color"],
47
+ ["border-radius", "--comic-panel-border-radius"],
48
+ ["border-style", "--comic-panel-border-style"],
49
+ ["border-width", "--comic-panel-border-width"],
50
+ ["box-shadow", "--comic-panel-shadow"],
51
+ ]);
52
+
53
+ function parseStyle(source, styleProperties, context) {
54
+ const declarations = [];
55
+ const properties = new Set();
56
+ const declarationPattern = /([a-z][a-z-]*)\s*:\s*([^;]+)(?:;|$)/gy;
57
+ let offset = 0;
58
+
59
+ while (offset < source.length) {
60
+ while (/\s/.test(source[offset])) offset += 1;
61
+ if (offset === source.length) break;
62
+
63
+ declarationPattern.lastIndex = offset;
64
+ const match = declarationPattern.exec(source);
65
+ if (!match) throw new Error(`Invalid ${context} style near: ${source.slice(offset)}`);
66
+
67
+ const property = match[1];
68
+ const value = match[2].trim();
69
+ if (!styleProperties.has(property)) {
70
+ throw new Error(`Unsupported ${context} style property: ${property}`);
71
+ }
72
+ if (properties.has(property)) {
73
+ throw new Error(`Duplicate ${context} style property: ${property}`);
74
+ }
75
+ if (!value || /[{}]|url\s*\(|expression\s*\(|@import|javascript\s*:/i.test(value)) {
76
+ throw new Error(`Invalid value for ${context} style property: ${property}`);
77
+ }
78
+
79
+ properties.add(property);
80
+ declarations.push(`${styleProperties.get(property)}: ${value};`);
81
+ offset = declarationPattern.lastIndex;
82
+ }
83
+
84
+ return declarations;
85
+ }
86
+
12
87
  function parsePanelAttributes(source) {
13
88
  const attributes = {};
14
89
  const attributePattern = /([a-z][a-z-]*)="([^"]*)"/gy;
@@ -21,7 +96,7 @@ function parsePanelAttributes(source) {
21
96
  attributePattern.lastIndex = offset;
22
97
  const match = attributePattern.exec(source);
23
98
  if (!match) throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
24
- if (match[1] !== "columns" && match[1] !== "label") {
99
+ if (!["columns", "label", "boxed", "style"].includes(match[1])) {
25
100
  throw new Error(`Unsupported panels attribute: ${match[1]}`);
26
101
  }
27
102
  if (Object.hasOwn(attributes, match[1])) {
@@ -36,36 +111,132 @@ function parsePanelAttributes(source) {
36
111
  throw new Error("Panel columns must be an integer from 1 to 6");
37
112
  }
38
113
 
39
- return { columns, label: attributes.label };
114
+ if (attributes.boxed !== undefined && attributes.boxed !== "true") {
115
+ throw new Error('Panel boxed must be "true"');
116
+ }
117
+
118
+ const style = parseStyle(attributes.style || "", panelStyleProperties, "panel");
119
+ if (attributes.boxed !== "true" && style.some((declaration) => !declaration.startsWith("--panel-gap:"))) {
120
+ throw new Error('Panel card styles require boxed="true"');
121
+ }
122
+
123
+ return {
124
+ columns,
125
+ label: attributes.label,
126
+ boxed: attributes.boxed === "true",
127
+ style,
128
+ };
129
+ }
130
+
131
+ function parseComicPanelAttributes(source) {
132
+ const attributes = {};
133
+ const attributePattern = /([a-z][a-z-]*)="([^"]*)"/gy;
134
+ let offset = 0;
135
+
136
+ while (offset < source.length) {
137
+ while (source[offset] === " " || source[offset] === "\t") offset += 1;
138
+ if (offset === source.length) break;
139
+
140
+ attributePattern.lastIndex = offset;
141
+ const match = attributePattern.exec(source);
142
+ if (!match) throw new Error(`Invalid panel attribute near: ${source.slice(offset)}`);
143
+ if (!["label", "divider", "style"].includes(match[1])) {
144
+ throw new Error(`Unsupported panel attribute: ${match[1]}`);
145
+ }
146
+ if (Object.hasOwn(attributes, match[1])) {
147
+ throw new Error(`Duplicate panel attribute: ${match[1]}`);
148
+ }
149
+ attributes[match[1]] = match[2];
150
+ offset = attributePattern.lastIndex;
151
+ }
152
+
153
+ if (attributes.divider !== undefined && attributes.divider !== "true") {
154
+ throw new Error('Panel divider must be "true"');
155
+ }
156
+
157
+ return {
158
+ label: attributes.label,
159
+ divider: attributes.divider === "true",
160
+ style: parseStyle(attributes.style || "", comicPanelStyleProperties, "panel"),
161
+ };
40
162
  }
41
163
 
42
164
  const panelsExtension = {
43
165
  name: "panels",
44
166
  level: "block",
45
167
  start(source) {
46
- return source.match(/^:::panels(?:[ \t]|$)/m)?.index;
168
+ return source.match(/^:{3,}panels(?:[ \t]|$)/m)?.index;
47
169
  },
48
170
  tokenizer(source) {
49
- const match = /^:::panels([^\n]*)\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
50
- if (!match) return undefined;
171
+ const container = matchFencedContainer(source, "panels");
172
+ if (!container) return undefined;
51
173
 
52
- const attributes = parsePanelAttributes(match[1].trim());
174
+ const attributes = parsePanelAttributes(container.attributes);
175
+ const tokens = this.lexer.blockTokens(container.content);
53
176
  return {
54
177
  type: "panels",
55
- raw: match[0],
178
+ raw: container.raw,
56
179
  columns: attributes.columns,
57
180
  label: attributes.label,
58
- tokens: this.lexer.blockTokens(match[2]),
181
+ boxed: attributes.boxed,
182
+ style: attributes.style,
183
+ nested: tokens.some((token) => token.type === "panel"),
184
+ tokens,
59
185
  };
60
186
  },
61
187
  renderer(token) {
62
188
  const label = token.label
63
189
  ? ` role="group" aria-label="${escapeAttribute(token.label)}"`
64
190
  : "";
65
- return `<div class="panel-group" style="--panel-columns: ${token.columns};"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
191
+ const className = [
192
+ "panel-group",
193
+ token.boxed ? "panel-group-boxed" : "",
194
+ token.nested ? "panel-group-nested" : "",
195
+ ].filter(Boolean).join(" ");
196
+ const style = [`--panel-columns: ${token.columns};`, ...token.style].join(" ");
197
+ return `<div class="${className}" style="${escapeAttribute(style)}"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
66
198
  },
67
199
  };
68
200
 
201
+ function createComicPanelExtension(name, type) {
202
+ return {
203
+ name: type,
204
+ level: "block",
205
+ start(source) {
206
+ return source.match(new RegExp(`^:{3,}${name}(?:[ \\t]|$)`, "m"))?.index;
207
+ },
208
+ tokenizer(source) {
209
+ const container = matchFencedContainer(source, name);
210
+ if (!container) return undefined;
211
+
212
+ const attributes = parseComicPanelAttributes(container.attributes);
213
+
214
+ return {
215
+ type,
216
+ raw: container.raw,
217
+ label: attributes.label,
218
+ divider: attributes.divider,
219
+ style: attributes.style,
220
+ tokens: this.lexer.blockTokens(container.content),
221
+ };
222
+ },
223
+ renderer(token) {
224
+ const label = token.label
225
+ ? ` aria-label="${escapeAttribute(token.label)}"`
226
+ : "";
227
+ const style = token.style.length
228
+ ? ` style="${escapeAttribute(token.style.join(" "))}"`
229
+ : "";
230
+ const className = token.divider
231
+ ? "comic-panel comic-panel-divider"
232
+ : "comic-panel";
233
+ return `<section class="${className}"${style}${label}>\n${this.parser.parse(token.tokens)}</section>\n`;
234
+ },
235
+ };
236
+ }
237
+
238
+ const panelExtension = createComicPanelExtension("panel", "panel");
239
+
69
240
  marked.setOptions({
70
241
  renderer: new marked.Renderer(),
71
242
  pedantic: false,
@@ -179,7 +350,7 @@ const renderer = {
179
350
  };
180
351
 
181
352
  marked.use({
182
- extensions: [panelsExtension],
353
+ extensions: [panelsExtension, panelExtension],
183
354
  renderer,
184
355
  hooks: {
185
356
  postprocess(html) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossbook",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A lightweight static blog site generator for GitHub Pages",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -227,11 +227,23 @@
227
227
  .panel-group {
228
228
  display: grid;
229
229
  grid-template-columns: repeat(var(--panel-columns), minmax(0, 1fr));
230
- gap: 1.5rem;
230
+ gap: var(--panel-gap, 1.5rem);
231
231
  align-items: start;
232
232
  margin: 1.5rem 0;
233
233
  }
234
234
 
235
+ .panel-group-nested {
236
+ align-items: stretch;
237
+ grid-auto-rows: 1fr;
238
+ }
239
+
240
+ .panel-group-nested > .comic-panel {
241
+ box-sizing: border-box;
242
+ min-width: 0;
243
+ height: 100%;
244
+ margin: 0;
245
+ }
246
+
235
247
  .panel-group .image-container,
236
248
  .panel-group figure {
237
249
  min-width: 0;
@@ -254,6 +266,101 @@
254
266
  text-align: center;
255
267
  }
256
268
 
269
+ .panel-group-boxed {
270
+ align-items: stretch;
271
+ grid-auto-rows: 1fr;
272
+ gap: var(--panel-gap, 0.75rem);
273
+ }
274
+
275
+ .panel-group-boxed > .image-container {
276
+ height: 100%;
277
+ }
278
+
279
+ .panel-group-boxed > .image-container > figure {
280
+ height: 100%;
281
+ display: flex;
282
+ flex-direction: column;
283
+ border: var(--panel-border, 1px solid #575752);
284
+ border-radius: var(--panel-border-radius, 0);
285
+ background: var(--panel-background, #fff);
286
+ box-shadow: var(--panel-box-shadow, none);
287
+ }
288
+
289
+ .panel-group-boxed[style*="--panel-border-color:"] > .image-container > figure {
290
+ border-color: var(--panel-border-color);
291
+ }
292
+
293
+ .panel-group-boxed[style*="--panel-border-style:"] > .image-container > figure {
294
+ border-style: var(--panel-border-style);
295
+ }
296
+
297
+ .panel-group-boxed[style*="--panel-border-width:"] > .image-container > figure {
298
+ border-width: var(--panel-border-width);
299
+ }
300
+
301
+ .panel-group-boxed[style*="--panel-background-color:"] > .image-container > figure {
302
+ background-color: var(--panel-background-color);
303
+ }
304
+
305
+ .panel-group-boxed > .image-container > figure > figcaption {
306
+ flex: 1;
307
+ margin-top: 0;
308
+ padding: 0.75rem;
309
+ font-size: 0.9rem;
310
+ }
311
+
312
+ .comic-panel {
313
+ margin: 2rem 0;
314
+ padding: 1rem;
315
+ border: var(--comic-panel-border, 2px solid #575752);
316
+ border-radius: var(--comic-panel-border-radius, 0);
317
+ background: var(--comic-panel-background, #fff);
318
+ box-shadow: var(--comic-panel-shadow, 4px 4px 0 rgba(70, 70, 66, 0.14));
319
+ }
320
+
321
+ .comic-panel[style*="--comic-panel-border-color:"] {
322
+ border-color: var(--comic-panel-border-color);
323
+ }
324
+
325
+ .comic-panel[style*="--comic-panel-border-style:"] {
326
+ border-style: var(--comic-panel-border-style);
327
+ }
328
+
329
+ .comic-panel[style*="--comic-panel-border-width:"] {
330
+ border-width: var(--comic-panel-border-width);
331
+ }
332
+
333
+ .comic-panel[style*="--comic-panel-background-color:"] {
334
+ background-color: var(--comic-panel-background-color);
335
+ }
336
+
337
+ .comic-panel > :first-child {
338
+ margin-top: 0;
339
+ }
340
+
341
+ .comic-panel > :last-child {
342
+ margin-bottom: 0;
343
+ }
344
+
345
+ .comic-panel .image-container,
346
+ .comic-panel figure {
347
+ display: block;
348
+ width: 100%;
349
+ }
350
+
351
+ .comic-panel figure {
352
+ padding: 0;
353
+ }
354
+
355
+ .comic-panel .image-dialogue {
356
+ margin-bottom: 1rem;
357
+ }
358
+
359
+ .comic-panel-divider .image-dialogue {
360
+ padding-bottom: 1rem;
361
+ border-bottom: 1px solid #575752;
362
+ }
363
+
257
364
  @media screen and (max-width: 599px) {
258
365
  .panel-group {
259
366
  grid-template-columns: 1fr;
@@ -262,6 +369,21 @@
262
369
  .sized-image {
263
370
  width: 100% !important;
264
371
  }
372
+
373
+ .comic-panel {
374
+ margin: 1.5rem 0;
375
+ padding: 0.75rem;
376
+ border-width: 1px;
377
+ box-shadow: 2px 2px 0 rgba(70, 70, 66, 0.14);
378
+ }
379
+ }
380
+
381
+ .comic-panel[style*="--comic-panel-border:"] {
382
+ border: var(--comic-panel-border);
383
+ }
384
+
385
+ .comic-panel[style*="--comic-panel-shadow:"] {
386
+ box-shadow: var(--comic-panel-shadow);
265
387
  }
266
388
 
267
389
  @media screen and (min-width: 600px) {
@@ -58,7 +58,7 @@
58
58
  </div>
59
59
  ${page.body.includes('class="blockquote-container image-dialogue"') ? `<div class="transcript-control">
60
60
  <div class="transcript-toggle">
61
- <label for="comic-transcript-toggle">Comic transcript</label>
61
+ <label for="comic-transcript-toggle">${page.config.transcriptLabel || "Display dialogue text"}</label>
62
62
  <input id="comic-transcript-toggle" type="checkbox" role="switch" aria-controls="comic-transcript" checked>
63
63
  </div>
64
64
  </div>` : ""}