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/README.md +755 -576
- package/bin/fossbook.js +73 -59
- package/lib/deploy.js +2 -1
- package/lib/index.js +58 -29
- package/lib/init.js +103 -19
- package/lib/mod/config.js +43 -13
- package/lib/mod/github_cache.js +130 -0
- package/lib/mod/image.js +411 -0
- package/lib/mod/marked.js +65 -25
- package/lib/mod/page.js +78 -29
- package/lib/posts.js +1 -0
- package/package.json +49 -48
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(
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
111
|
-
|
|
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)
|
|
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 =
|
|
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(
|
|
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)
|
|
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(
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
].
|
|
223
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
]
|
|
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 =
|
|
396
|
-
|
|
397
|
-
|
|
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, {
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
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
|
+
}
|