fossbook 0.2.14 → 0.2.16

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
@@ -53,12 +53,15 @@ function renderKoreanBlockquoteQuotes(html) {
53
53
  }
54
54
 
55
55
  function matchFencedContainer(source, name) {
56
- const opening = new RegExp(`^(:{3,})${name}(?=[ \\t]|\\n)([^\\n]*)\\n`).exec(source);
56
+ const opening = new RegExp(`^(:{3,})${name}(?=[ \\t]|\\n)([^\\n]*)\\n`).exec(
57
+ source,
58
+ );
57
59
  if (!opening) return undefined;
58
60
 
59
61
  const contentStart = opening[0].length;
60
- const closing = new RegExp(`^${opening[1]}[ \\t]*(?:\\n|$)`, "m")
61
- .exec(source.slice(contentStart));
62
+ const closing = new RegExp(`^${opening[1]}[ \\t]*(?:\\n|$)`, "m").exec(
63
+ source.slice(contentStart),
64
+ );
62
65
  if (!closing) return undefined;
63
66
 
64
67
  const contentEnd = contentStart + closing.index;
@@ -70,9 +73,7 @@ function matchFencedContainer(source, name) {
70
73
  };
71
74
  }
72
75
 
73
- const panelGroupStyleProperties = new Map([
74
- ["gap", "--panel-gap"],
75
- ]);
76
+ const panelGroupStyleProperties = new Map([["gap", "--panel-gap"]]);
76
77
 
77
78
  const comicPanelStyleProperties = new Map([
78
79
  ["background", "--comic-panel-background"],
@@ -97,7 +98,8 @@ function parseStyle(source, styleProperties, context) {
97
98
 
98
99
  declarationPattern.lastIndex = offset;
99
100
  const match = declarationPattern.exec(source);
100
- if (!match) throw new Error(`Invalid ${context} style near: ${source.slice(offset)}`);
101
+ if (!match)
102
+ throw new Error(`Invalid ${context} style near: ${source.slice(offset)}`);
101
103
 
102
104
  const property = match[1];
103
105
  const value = match[2].trim();
@@ -107,8 +109,13 @@ function parseStyle(source, styleProperties, context) {
107
109
  if (properties.has(property)) {
108
110
  throw new Error(`Duplicate ${context} style property: ${property}`);
109
111
  }
110
- if (!value || /[{}]|url\s*\(|expression\s*\(|@import|javascript\s*:/i.test(value)) {
111
- throw new Error(`Invalid value for ${context} style property: ${property}`);
112
+ if (
113
+ !value ||
114
+ /[{}]|url\s*\(|expression\s*\(|@import|javascript\s*:/i.test(value)
115
+ ) {
116
+ throw new Error(
117
+ `Invalid value for ${context} style property: ${property}`,
118
+ );
112
119
  }
113
120
 
114
121
  properties.add(property);
@@ -130,7 +137,8 @@ function parsePanelAttributes(source) {
130
137
 
131
138
  attributePattern.lastIndex = offset;
132
139
  const match = attributePattern.exec(source);
133
- if (!match) throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
140
+ if (!match)
141
+ throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
134
142
  if (!["columns", "label", "style"].includes(match[1])) {
135
143
  throw new Error(`Unsupported panels attribute: ${match[1]}`);
136
144
  }
@@ -141,7 +149,8 @@ function parsePanelAttributes(source) {
141
149
  offset = attributePattern.lastIndex;
142
150
  }
143
151
 
144
- const columns = attributes.columns === undefined ? 2 : Number(attributes.columns);
152
+ const columns =
153
+ attributes.columns === undefined ? 2 : Number(attributes.columns);
145
154
  if (!Number.isInteger(columns) || columns < 1 || columns > 6) {
146
155
  throw new Error("Panel columns must be an integer from 1 to 6");
147
156
  }
@@ -149,7 +158,11 @@ function parsePanelAttributes(source) {
149
158
  return {
150
159
  columns,
151
160
  label: attributes.label,
152
- style: parseStyle(attributes.style || "", panelGroupStyleProperties, "panel group"),
161
+ style: parseStyle(
162
+ attributes.style || "",
163
+ panelGroupStyleProperties,
164
+ "panel group",
165
+ ),
153
166
  };
154
167
  }
155
168
 
@@ -164,7 +177,8 @@ function parseComicPanelAttributes(source) {
164
177
 
165
178
  attributePattern.lastIndex = offset;
166
179
  const match = attributePattern.exec(source);
167
- if (!match) throw new Error(`Invalid panel attribute near: ${source.slice(offset)}`);
180
+ if (!match)
181
+ throw new Error(`Invalid panel attribute near: ${source.slice(offset)}`);
168
182
  if (!["label", "divider", "rounded", "style"].includes(match[1])) {
169
183
  throw new Error(`Unsupported panel attribute: ${match[1]}`);
170
184
  }
@@ -186,7 +200,11 @@ function parseComicPanelAttributes(source) {
186
200
  label: attributes.label,
187
201
  divider: attributes.divider === "true",
188
202
  rounded: attributes.rounded === "true",
189
- style: parseStyle(attributes.style || "", comicPanelStyleProperties, "panel"),
203
+ style: parseStyle(
204
+ attributes.style || "",
205
+ comicPanelStyleProperties,
206
+ "panel",
207
+ ),
190
208
  };
191
209
  }
192
210
 
@@ -216,11 +234,12 @@ const panelsExtension = {
216
234
  const label = token.label
217
235
  ? ` role="group" aria-label="${escapeAttribute(token.label)}"`
218
236
  : "";
219
- const className = [
220
- "panel-group",
221
- token.nested ? "panel-group-nested" : "",
222
- ].filter(Boolean).join(" ");
223
- const style = [`--panel-columns: ${token.columns};`, ...token.style].join(" ");
237
+ const className = ["panel-group", token.nested ? "panel-group-nested" : ""]
238
+ .filter(Boolean)
239
+ .join(" ");
240
+ const style = [`--panel-columns: ${token.columns};`, ...token.style].join(
241
+ " ",
242
+ );
224
243
  return `<div class="${className}" style="${escapeAttribute(style)}"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
225
244
  },
226
245
  };
@@ -232,7 +251,10 @@ const wikipediaUrlExtension = {
232
251
  return source.search(/https?:\/\/[a-z-]+\.(?:m\.)?wikipedia\.org\/wiki\//i);
233
252
  },
234
253
  tokenizer(source) {
235
- const match = /^https?:\/\/[a-z-]+\.(?:m\.)?wikipedia\.org\/wiki\/[^\s<>"']+/i.exec(source);
254
+ const match =
255
+ /^https?:\/\/[a-z-]+\.(?:m\.)?wikipedia\.org\/wiki\/[^\s<>"']+/i.exec(
256
+ source,
257
+ );
236
258
  if (!match) return undefined;
237
259
 
238
260
  const raw = match[0].replace(/[.,;:!?]+$/, "");
@@ -252,7 +274,7 @@ function createComicPanelExtension(name, type) {
252
274
  start(source) {
253
275
  return source.match(new RegExp(`^:{3,}${name}(?:[ \\t]|$)`, "m"))?.index;
254
276
  },
255
- tokenizer(source) {
277
+ tokenizer(source) {
256
278
  const container = matchFencedContainer(source, name);
257
279
  if (!container) return undefined;
258
280
 
@@ -279,7 +301,9 @@ function createComicPanelExtension(name, type) {
279
301
  "comic-panel",
280
302
  token.divider ? "comic-panel-divider" : "",
281
303
  token.rounded ? "comic-panel-rounded" : "",
282
- ].filter(Boolean).join(" ");
304
+ ]
305
+ .filter(Boolean)
306
+ .join(" ");
283
307
  return `<section class="${className}"${style}${label}>\n${this.parser.parse(token.tokens)}</section>\n`;
284
308
  },
285
309
  };
@@ -328,6 +352,18 @@ const renderer = {
328
352
  return `<pre><code class="hljs${cls}">${value}</code></pre>`;
329
353
  },
330
354
  image(href, title, text) {
355
+ const basePath = this.options.basePath?.replace(/\/$/, "") || "";
356
+ if (
357
+ href.startsWith("/") &&
358
+ !href.startsWith("//") &&
359
+ basePath &&
360
+ basePath !== "/" &&
361
+ href !== basePath &&
362
+ !href.startsWith(`${basePath}/`)
363
+ ) {
364
+ href = `${basePath}${href}`;
365
+ }
366
+
331
367
  let size = null;
332
368
  // Check if the title contains a size specification
333
369
  if (title && title.includes("size:")) {
@@ -348,6 +384,9 @@ const renderer = {
348
384
  title = title.replace(/align:(left|right|center)/g, "").trim();
349
385
  }
350
386
  }
387
+ if (title && title.includes("publish-width:")) {
388
+ title = title.replace(/publish-width:\d+/g, "").trim();
389
+ }
351
390
 
352
391
  // Construct the image tag with optional size and title
353
392
  let imageTag = `<img src="${href}" alt="${text}"`;
@@ -392,9 +431,10 @@ const renderer = {
392
431
  }
393
432
  },
394
433
  blockquote(quote) {
395
- const content = this.options.language === "ko"
396
- ? renderKoreanBlockquoteQuotes(quote)
397
- : quote;
434
+ const content =
435
+ this.options.language === "ko"
436
+ ? renderKoreanBlockquoteQuotes(quote)
437
+ : quote;
398
438
  return `<div class="blockquote-container"><blockquote>${content}</blockquote></div>`;
399
439
  },
400
440
  paragraph(text) {
package/lib/mod/page.js CHANGED
@@ -3,6 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const marked = require("./marked");
5
5
  const PageBase = require("./page_base");
6
+ const { publishImage } = require("./image");
6
7
 
7
8
  module.exports = class Page extends PageBase {
8
9
  constructor(config) {
@@ -33,12 +34,24 @@ module.exports = class Page extends PageBase {
33
34
  this.slug = this.path;
34
35
  // parsed content by fields and body
35
36
  const content = fm(mdContent);
37
+ this.imageWidthOverrides = new Map();
38
+ const imagePattern = /!\[[^\]]*\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g;
39
+ for (const match of content.body.matchAll(imagePattern)) {
40
+ const widthMatch = match[2]?.match(/publish-width:(\d+)/);
41
+ if (widthMatch) {
42
+ this.imageWidthOverrides.set(
43
+ path.basename(match[1]),
44
+ Number(widthMatch[1]),
45
+ );
46
+ }
47
+ }
36
48
 
37
49
  this.title = `${content.attributes.title}`;
38
50
  this.date = content.attributes.date;
39
51
  this.url = `${this.config.blogsite}/${this.path}/`;
40
52
  this.image = content.attributes.image;
41
53
  this.description = content.attributes.description;
54
+ this.draft = content.attributes.draft === true;
42
55
  // default image if no imageURL is specified
43
56
  this.imageURL = this.config.image;
44
57
 
@@ -50,7 +63,10 @@ module.exports = class Page extends PageBase {
50
63
  }
51
64
 
52
65
  // generated HTML from markdown
53
- this.body = marked.parse(content.body, { language: this.config.language });
66
+ this.body = marked.parse(content.body, {
67
+ language: this.config.language,
68
+ basePath: this.config.basePath,
69
+ });
54
70
  // remove <p></p> and <p> </p> from the beginning and end of the content.body
55
71
  this.body = this.body.replace(/<p><\/p>/g, "").replace(/<p> <\/p>/g, "");
56
72
 
@@ -60,9 +76,10 @@ module.exports = class Page extends PageBase {
60
76
  }
61
77
 
62
78
  // about/index.html or about/hello.html
63
- generateContent(templateFile, outputPath) {
79
+ async generateContent(templateFile, outputPath) {
64
80
  // For a series of posts
65
- if (outputPath === undefined) outputPath = path.join(this.path, "index.html");
81
+ if (outputPath === undefined)
82
+ outputPath = path.join(this.path, "index.html");
66
83
  // if file name is not included in the path
67
84
  if (outputPath.indexOf(".htm") === -1) {
68
85
  outputPath = path.join(outputPath, "index.html");
@@ -71,8 +88,7 @@ module.exports = class Page extends PageBase {
71
88
  const outputDir = path.dirname(outputPath);
72
89
  // if a directory path is included in the path
73
90
  if (outputDir !== ".") {
74
- if (this.path === "")
75
- this.path = outputDir;
91
+ if (this.path === "") this.path = outputDir;
76
92
 
77
93
  const outPath = path.join(this.config.dev.outdir, this.path);
78
94
  if (fs.existsSync(outPath))
@@ -84,11 +100,43 @@ module.exports = class Page extends PageBase {
84
100
  } else {
85
101
  // remove the outputPath file if it exists
86
102
  const outPath = path.join(this.config.dev.outdir, this.path);
87
- if (fs.existsSync(outPath))
88
- fs.unlinkSync(outPath);
103
+ if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
104
+ }
105
+
106
+ // if there is the images folder in the output directory.
107
+ const srcImagesDir = path.join(
108
+ this.config.dev.postsdir,
109
+ this.slug,
110
+ "images",
111
+ );
112
+ if (fs.existsSync(srcImagesDir) && this.path !== "") {
113
+ // Copy images folder from postsdir to outdir
114
+ const destImagesDir = path.join(
115
+ this.config.dev.outdir,
116
+ this.path,
117
+ "images",
118
+ );
119
+ if (!fs.existsSync(destImagesDir))
120
+ fs.mkdirSync(destImagesDir, { recursive: true });
121
+
122
+ const publishedImages = new Map();
123
+ for (const image of fs.readdirSync(srcImagesDir)) {
124
+ const publishedImage = await publishImage(
125
+ path.join(srcImagesDir, image),
126
+ path.join(destImagesDir, image),
127
+ this.config,
128
+ this.imageWidthOverrides.get(image),
129
+ );
130
+ if (publishedImage) publishedImages.set(image, publishedImage);
131
+ }
132
+ this.body = addWebPPictureSources(this.body, publishedImages);
89
133
  }
90
134
 
91
- const layoutsPath = path.join(this.config.themePath, "layouts", templateFile);
135
+ const layoutsPath = path.join(
136
+ this.config.themePath,
137
+ "layouts",
138
+ templateFile,
139
+ );
92
140
  const postHTML = this.generateHTML(layoutsPath);
93
141
 
94
142
  fs.writeFileSync(
@@ -99,26 +147,27 @@ module.exports = class Page extends PageBase {
99
147
  console.log(`${outputPath}/index.html was created successfully`);
100
148
  },
101
149
  );
102
-
103
- // if there is the images folder in the output directory.
104
- const srcImagesDir = path.join(this.config.dev.postsdir, this.slug, "images");
105
- if (
106
- fs.existsSync(srcImagesDir) &&
107
- this.path !== ""
108
- ) {
109
- // Copy images folder from postsdir to outdir
110
- const destImagesDir = path.join(this.config.dev.outdir, this.path, "images");
111
- if (!fs.existsSync(destImagesDir))
112
- fs.mkdirSync(destImagesDir, { recursive: true });
113
-
114
- fs.readdirSync(srcImagesDir).forEach(
115
- (image) => {
116
- fs.copyFileSync(
117
- path.join(srcImagesDir, image),
118
- path.join(destImagesDir, image),
119
- );
120
- },
121
- );
122
- }
123
150
  }
124
151
  };
152
+
153
+ function addWebPPictureSources(html, publishedImages) {
154
+ return html.replace(/<img src="([^"]+\.png)"([^>]*)>/gi, (imageTag, href) => {
155
+ const hrefWithoutQuery = href.split(/[?#]/, 1)[0];
156
+ const fileName = decodeURIComponent(
157
+ hrefWithoutQuery.replace(/\\/g, "/").split("/").pop(),
158
+ );
159
+ const publishedImage = publishedImages.get(fileName);
160
+ if (!publishedImage || publishedImage.webp.length === 0) return imageTag;
161
+
162
+ const lastSlash = hrefWithoutQuery.lastIndexOf("/");
163
+ const hrefPrefix =
164
+ lastSlash === -1 ? "" : hrefWithoutQuery.slice(0, lastSlash + 1);
165
+ const srcset = publishedImage.webp
166
+ .map(
167
+ (candidate) =>
168
+ `${hrefPrefix}${encodeURIComponent(candidate.fileName)} ${candidate.width}w`,
169
+ )
170
+ .join(", ");
171
+ return `<picture><source type="image/webp" srcset="${srcset}" sizes="(max-width: 599px) 100vw, 700px">${imageTag}</picture>`;
172
+ });
173
+ }
package/lib/posts.js CHANGED
@@ -26,6 +26,7 @@ module.exports = class Posts {
26
26
  }
27
27
  const post = new Page(this.config);
28
28
  post.readSource(indexPath, postPath);
29
+ if (post.draft && !this.config.includeDrafts) return;
29
30
  // Optionally nest posts under a URL prefix (e.g. "posts" -> /posts/<slug>/).
30
31
  // post.slug stays the source folder name so image lookups remain correct.
31
32
  const prefix = this.config.postsPath ? `${this.config.postsPath}/` : "";
package/package.json CHANGED
@@ -1,48 +1,49 @@
1
- {
2
- "name": "fossbook",
3
- "version": "0.2.14",
4
- "description": "A Markdown-based authoring and web-publishing tool for comics, illustrated stories, and articles",
5
- "main": "lib/index.js",
6
- "scripts": {
7
- "build": "node bin/fossbook.js build",
8
- "start": "node bin/fossbook.js serve",
9
- "test": "mocha test",
10
- "prettier": "prettier --write ."
11
- },
12
- "keywords": [
13
- "comic-authoring",
14
- "webcomics",
15
- "publishing",
16
- "static-site-generator",
17
- "github-pages",
18
- "markdown"
19
- ],
20
- "author": "Joone Hur",
21
- "license": "BSD-3-Clause",
22
- "dependencies": {
23
- "express": "^4.19.1",
24
- "front-matter": "^4.0.2",
25
- "highlight.js": "^11.9.0",
26
- "marked": "^12.0.1"
27
- },
28
- "devDependencies": {
29
- "mocha": "^10.2.0",
30
- "prettier": "^3.2.5"
31
- },
32
- "bin": {
33
- "fossbook": "bin/fossbook.js"
34
- },
35
- "engines": {
36
- "node": ">=18"
37
- },
38
- "files": [
39
- "bin/",
40
- "docs/images/",
41
- "lib/",
42
- "themes/"
43
- ],
44
- "repository": {
45
- "type": "git",
46
- "url": "https://github.com/joone/fossbook"
47
- }
48
- }
1
+ {
2
+ "name": "fossbook",
3
+ "version": "0.2.16",
4
+ "description": "A Markdown-based authoring and web-publishing tool for comics, illustrated stories, and articles",
5
+ "main": "lib/index.js",
6
+ "scripts": {
7
+ "build": "node bin/fossbook.js build",
8
+ "start": "node bin/fossbook.js serve",
9
+ "test": "mocha test",
10
+ "prettier": "prettier --write ."
11
+ },
12
+ "keywords": [
13
+ "comic-authoring",
14
+ "webcomics",
15
+ "publishing",
16
+ "static-site-generator",
17
+ "github-pages",
18
+ "markdown"
19
+ ],
20
+ "author": "Joone Hur",
21
+ "license": "BSD-3-Clause",
22
+ "dependencies": {
23
+ "express": "^4.19.1",
24
+ "front-matter": "^4.0.2",
25
+ "highlight.js": "^11.9.0",
26
+ "marked": "^12.0.1",
27
+ "sharp": "^0.35.4"
28
+ },
29
+ "devDependencies": {
30
+ "mocha": "^10.2.0",
31
+ "prettier": "^3.2.5"
32
+ },
33
+ "bin": {
34
+ "fossbook": "bin/fossbook.js"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "files": [
40
+ "bin/",
41
+ "docs/images/",
42
+ "lib/",
43
+ "themes/"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/joone/fossbook"
48
+ }
49
+ }