fossbook 0.2.3 → 0.2.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/lib/mod/marked.js CHANGED
@@ -9,6 +9,24 @@ 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
+
12
30
  const panelStyleProperties = new Map([
13
31
  ["background", "--panel-background"],
14
32
  ["background-color", "--panel-background-color"],
@@ -21,7 +39,18 @@ const panelStyleProperties = new Map([
21
39
  ["gap", "--panel-gap"],
22
40
  ]);
23
41
 
24
- function parsePanelStyle(source) {
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) {
25
54
  const declarations = [];
26
55
  const properties = new Set();
27
56
  const declarationPattern = /([a-z][a-z-]*)\s*:\s*([^;]+)(?:;|$)/gy;
@@ -33,22 +62,22 @@ function parsePanelStyle(source) {
33
62
 
34
63
  declarationPattern.lastIndex = offset;
35
64
  const match = declarationPattern.exec(source);
36
- if (!match) throw new Error(`Invalid panel style near: ${source.slice(offset)}`);
65
+ if (!match) throw new Error(`Invalid ${context} style near: ${source.slice(offset)}`);
37
66
 
38
67
  const property = match[1];
39
68
  const value = match[2].trim();
40
- if (!panelStyleProperties.has(property)) {
41
- throw new Error(`Unsupported panel style property: ${property}`);
69
+ if (!styleProperties.has(property)) {
70
+ throw new Error(`Unsupported ${context} style property: ${property}`);
42
71
  }
43
72
  if (properties.has(property)) {
44
- throw new Error(`Duplicate panel style property: ${property}`);
73
+ throw new Error(`Duplicate ${context} style property: ${property}`);
45
74
  }
46
75
  if (!value || /[{}]|url\s*\(|expression\s*\(|@import|javascript\s*:/i.test(value)) {
47
- throw new Error(`Invalid value for panel style property: ${property}`);
76
+ throw new Error(`Invalid value for ${context} style property: ${property}`);
48
77
  }
49
78
 
50
79
  properties.add(property);
51
- declarations.push(`${panelStyleProperties.get(property)}: ${value};`);
80
+ declarations.push(`${styleProperties.get(property)}: ${value};`);
52
81
  offset = declarationPattern.lastIndex;
53
82
  }
54
83
 
@@ -86,15 +115,49 @@ function parsePanelAttributes(source) {
86
115
  throw new Error('Panel boxed must be "true"');
87
116
  }
88
117
 
89
- if (attributes.style !== undefined && attributes.boxed !== "true") {
90
- throw new Error('Panel style requires boxed="true"');
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"');
91
121
  }
92
122
 
93
123
  return {
94
124
  columns,
95
125
  label: attributes.label,
96
126
  boxed: attributes.boxed === "true",
97
- style: parsePanelStyle(attributes.style || ""),
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"),
98
161
  };
99
162
  }
100
163
 
@@ -102,57 +165,77 @@ const panelsExtension = {
102
165
  name: "panels",
103
166
  level: "block",
104
167
  start(source) {
105
- return source.match(/^:::panels(?:[ \t]|$)/m)?.index;
168
+ return source.match(/^:{3,}panels(?:[ \t]|$)/m)?.index;
106
169
  },
107
170
  tokenizer(source) {
108
- const match = /^:::panels([^\n]*)\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
109
- if (!match) return undefined;
171
+ const container = matchFencedContainer(source, "panels");
172
+ if (!container) return undefined;
110
173
 
111
- const attributes = parsePanelAttributes(match[1].trim());
174
+ const attributes = parsePanelAttributes(container.attributes);
175
+ const tokens = this.lexer.blockTokens(container.content);
112
176
  return {
113
177
  type: "panels",
114
- raw: match[0],
178
+ raw: container.raw,
115
179
  columns: attributes.columns,
116
180
  label: attributes.label,
117
181
  boxed: attributes.boxed,
118
182
  style: attributes.style,
119
- tokens: this.lexer.blockTokens(match[2]),
183
+ nested: tokens.some((token) => token.type === "panel"),
184
+ tokens,
120
185
  };
121
186
  },
122
187
  renderer(token) {
123
188
  const label = token.label
124
189
  ? ` role="group" aria-label="${escapeAttribute(token.label)}"`
125
190
  : "";
126
- const className = token.boxed ? "panel-group panel-group-boxed" : "panel-group";
191
+ const className = [
192
+ "panel-group",
193
+ token.boxed ? "panel-group-boxed" : "",
194
+ token.nested ? "panel-group-nested" : "",
195
+ ].filter(Boolean).join(" ");
127
196
  const style = [`--panel-columns: ${token.columns};`, ...token.style].join(" ");
128
197
  return `<div class="${className}" style="${escapeAttribute(style)}"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
129
198
  },
130
199
  };
131
200
 
132
- const comicBoxExtension = {
133
- name: "comicBox",
134
- level: "block",
135
- start(source) {
136
- return source.match(/^:::comic-box(?:[ \t]|$)/m)?.index;
137
- },
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
+ },
138
208
  tokenizer(source) {
139
- const match = /^:::comic-box(?:[ \t]+label="([^"]*)")?[ \t]*\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
140
- if (!match) return undefined;
209
+ const container = matchFencedContainer(source, name);
210
+ if (!container) return undefined;
141
211
 
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`;
154
- },
155
- };
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");
156
239
 
157
240
  marked.setOptions({
158
241
  renderer: new marked.Renderer(),
@@ -267,7 +350,7 @@ const renderer = {
267
350
  };
268
351
 
269
352
  marked.use({
270
- extensions: [panelsExtension, comicBoxExtension],
353
+ extensions: [panelsExtension, panelExtension],
271
354
  renderer,
272
355
  hooks: {
273
356
  postprocess(html) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossbook",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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;
@@ -260,71 +272,95 @@
260
272
  gap: var(--panel-gap, 0.75rem);
261
273
  }
262
274
 
263
- .panel-group-boxed .image-container {
275
+ .panel-group-boxed > .image-container {
264
276
  height: 100%;
265
277
  }
266
278
 
267
- .panel-group-boxed figure {
279
+ .panel-group-boxed > .image-container > figure {
268
280
  height: 100%;
269
281
  display: flex;
270
282
  flex-direction: column;
271
- border: var(--panel-border, 1px solid #575752);
283
+ border: 1px solid #575752;
272
284
  border-radius: var(--panel-border-radius, 0);
273
285
  background: var(--panel-background, #fff);
274
286
  box-shadow: var(--panel-box-shadow, none);
275
287
  }
276
288
 
277
- .panel-group-boxed[style*="--panel-border-color:"] figure {
289
+ .panel-group-boxed[style*="--panel-border:"] > .image-container > figure {
290
+ border: var(--panel-border);
291
+ }
292
+
293
+ .panel-group-boxed[style*="--panel-border-color:"] > .image-container > figure {
278
294
  border-color: var(--panel-border-color);
279
295
  }
280
296
 
281
- .panel-group-boxed[style*="--panel-border-style:"] figure {
297
+ .panel-group-boxed[style*="--panel-border-style:"] > .image-container > figure {
282
298
  border-style: var(--panel-border-style);
283
299
  }
284
300
 
285
- .panel-group-boxed[style*="--panel-border-width:"] figure {
301
+ .panel-group-boxed[style*="--panel-border-width:"] > .image-container > figure {
286
302
  border-width: var(--panel-border-width);
287
303
  }
288
304
 
289
- .panel-group-boxed[style*="--panel-background-color:"] figure {
305
+ .panel-group-boxed[style*="--panel-background-color:"] > .image-container > figure {
290
306
  background-color: var(--panel-background-color);
291
307
  }
292
308
 
293
- .panel-group-boxed figcaption {
309
+ .panel-group-boxed > .image-container > figure > figcaption {
294
310
  flex: 1;
295
311
  margin-top: 0;
296
312
  padding: 0.75rem;
297
313
  font-size: 0.9rem;
298
314
  }
299
315
 
300
- .comic-box {
316
+ .comic-panel {
301
317
  margin: 2rem 0;
302
318
  padding: 1rem;
303
319
  border: 2px solid #575752;
304
- background: #fff;
305
- box-shadow: 4px 4px 0 rgba(70, 70, 66, 0.14);
320
+ border-radius: var(--comic-panel-border-radius, 0);
321
+ background: var(--comic-panel-background, #fff);
322
+ box-shadow: var(--comic-panel-shadow, 4px 4px 0 rgba(70, 70, 66, 0.14));
306
323
  }
307
324
 
308
- .comic-box > :first-child {
325
+ .comic-panel[style*="--comic-panel-border-color:"] {
326
+ border-color: var(--comic-panel-border-color);
327
+ }
328
+
329
+ .comic-panel[style*="--comic-panel-border-style:"] {
330
+ border-style: var(--comic-panel-border-style);
331
+ }
332
+
333
+ .comic-panel[style*="--comic-panel-border-width:"] {
334
+ border-width: var(--comic-panel-border-width);
335
+ }
336
+
337
+ .comic-panel[style*="--comic-panel-background-color:"] {
338
+ background-color: var(--comic-panel-background-color);
339
+ }
340
+
341
+ .comic-panel > :first-child {
309
342
  margin-top: 0;
310
343
  }
311
344
 
312
- .comic-box > :last-child {
345
+ .comic-panel > :last-child {
313
346
  margin-bottom: 0;
314
347
  }
315
348
 
316
- .comic-box .image-container,
317
- .comic-box figure {
349
+ .comic-panel .image-container,
350
+ .comic-panel figure {
318
351
  display: block;
319
352
  width: 100%;
320
353
  }
321
354
 
322
- .comic-box figure {
355
+ .comic-panel figure {
323
356
  padding: 0;
324
357
  }
325
358
 
326
- .comic-box .image-dialogue {
359
+ .comic-panel .image-dialogue {
327
360
  margin-bottom: 1rem;
361
+ }
362
+
363
+ .comic-panel-divider .image-dialogue {
328
364
  padding-bottom: 1rem;
329
365
  border-bottom: 1px solid #575752;
330
366
  }
@@ -338,13 +374,21 @@
338
374
  width: 100% !important;
339
375
  }
340
376
 
341
- .comic-box {
377
+ .comic-panel {
342
378
  margin: 1.5rem 0;
343
379
  padding: 0.75rem;
344
380
  border-width: 1px;
345
381
  box-shadow: 2px 2px 0 rgba(70, 70, 66, 0.14);
346
382
  }
347
383
  }
384
+
385
+ .comic-panel[style*="--comic-panel-border:"] {
386
+ border: var(--comic-panel-border);
387
+ }
388
+
389
+ .comic-panel[style*="--comic-panel-shadow:"] {
390
+ box-shadow: var(--comic-panel-shadow);
391
+ }
348
392
 
349
393
  @media screen and (min-width: 600px) {
350
394
  figure {