fossbook 0.2.3 → 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/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.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;
@@ -260,11 +272,11 @@
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;
@@ -274,57 +286,77 @@
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-color:"] > .image-container > figure {
278
290
  border-color: var(--panel-border-color);
279
291
  }
280
292
 
281
- .panel-group-boxed[style*="--panel-border-style:"] figure {
293
+ .panel-group-boxed[style*="--panel-border-style:"] > .image-container > figure {
282
294
  border-style: var(--panel-border-style);
283
295
  }
284
296
 
285
- .panel-group-boxed[style*="--panel-border-width:"] figure {
297
+ .panel-group-boxed[style*="--panel-border-width:"] > .image-container > figure {
286
298
  border-width: var(--panel-border-width);
287
299
  }
288
300
 
289
- .panel-group-boxed[style*="--panel-background-color:"] figure {
301
+ .panel-group-boxed[style*="--panel-background-color:"] > .image-container > figure {
290
302
  background-color: var(--panel-background-color);
291
303
  }
292
304
 
293
- .panel-group-boxed figcaption {
305
+ .panel-group-boxed > .image-container > figure > figcaption {
294
306
  flex: 1;
295
307
  margin-top: 0;
296
308
  padding: 0.75rem;
297
309
  font-size: 0.9rem;
298
310
  }
299
311
 
300
- .comic-box {
312
+ .comic-panel {
301
313
  margin: 2rem 0;
302
314
  padding: 1rem;
303
- border: 2px solid #575752;
304
- background: #fff;
305
- box-shadow: 4px 4px 0 rgba(70, 70, 66, 0.14);
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);
306
323
  }
307
324
 
308
- .comic-box > :first-child {
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 {
309
338
  margin-top: 0;
310
339
  }
311
340
 
312
- .comic-box > :last-child {
341
+ .comic-panel > :last-child {
313
342
  margin-bottom: 0;
314
343
  }
315
344
 
316
- .comic-box .image-container,
317
- .comic-box figure {
345
+ .comic-panel .image-container,
346
+ .comic-panel figure {
318
347
  display: block;
319
348
  width: 100%;
320
349
  }
321
350
 
322
- .comic-box figure {
351
+ .comic-panel figure {
323
352
  padding: 0;
324
353
  }
325
354
 
326
- .comic-box .image-dialogue {
355
+ .comic-panel .image-dialogue {
327
356
  margin-bottom: 1rem;
357
+ }
358
+
359
+ .comic-panel-divider .image-dialogue {
328
360
  padding-bottom: 1rem;
329
361
  border-bottom: 1px solid #575752;
330
362
  }
@@ -338,13 +370,21 @@
338
370
  width: 100% !important;
339
371
  }
340
372
 
341
- .comic-box {
373
+ .comic-panel {
342
374
  margin: 1.5rem 0;
343
375
  padding: 0.75rem;
344
376
  border-width: 1px;
345
377
  box-shadow: 2px 2px 0 rgba(70, 70, 66, 0.14);
346
378
  }
347
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);
387
+ }
348
388
 
349
389
  @media screen and (min-width: 600px) {
350
390
  figure {