@ca-plant-list/ca-plant-list 0.3.7 → 0.4.1
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/.vscode/settings.json +7 -1
- package/data/synonyms.csv +95 -2
- package/data/taxa.csv +53 -22
- package/data/text/Antennaria-media.md +1 -0
- package/data/text/Antennaria-rosea-subsp-rosea.md +1 -0
- package/data/text/Camassia-leichtlinii-subsp-suksdorfii.md +1 -0
- package/data/text/Camassia-quamash-subsp-breviflora.md +1 -0
- package/data/text/Delphinium-glaucum.md +1 -0
- package/data/text/Delphinium-nuttallianum.md +1 -0
- package/data/text/Drymocallis-glandulosa-var-glandulosa.md +1 -0
- package/data/text/Drymocallis-lactea-var-austiniae.md +1 -0
- package/data/text/Erigeron-compositus.md +1 -0
- package/data/text/Erigeron-glacialis-var-glacialis.md +1 -0
- package/data/text/Eriogonum-incanum.md +1 -0
- package/data/text/Eriogonum-lobbii.md +1 -0
- package/data/text/Eriogonum-ovalifolium-var-nivale.md +1 -0
- package/data/text/Erythranthe-breweri.md +1 -0
- package/data/text/Erythranthe-erubescens.md +1 -0
- package/data/text/Navarretia-leptalea-subsp-bicolor.md +1 -0
- package/data/text/Navarretia-leptalea-subsp-leptalea.md +1 -0
- package/data/text/Polemonium-californicum.md +1 -0
- package/data/text/Polemonium-pulcherrimum-var-pulcherrimum.md +1 -0
- package/data/text/Primula-jeffreyi.md +1 -0
- package/data/text/Primula-tetrandra.md +1 -0
- package/data/text/Silene-douglasii-var-douglasii.md +1 -0
- package/data/text/Silene-lemmonii.md +1 -0
- package/data/text/Silene-sargentii.md +1 -0
- package/lib/basepagerenderer.js +3 -4
- package/lib/config.js +42 -19
- package/lib/csv.js +54 -36
- package/lib/ebook/ebook.js +84 -57
- package/lib/ebook/ebookpage.js +22 -11
- package/lib/ebook/ebooksitegenerator.js +36 -14
- package/lib/ebook/glossarypages.js +20 -17
- package/lib/ebook/images.js +13 -5
- package/lib/ebook/pages/page_list_families.js +0 -2
- package/lib/ebook/pages/page_list_flower_color.js +14 -9
- package/lib/ebook/pages/page_list_flowers.js +59 -41
- package/lib/ebook/pages/taxonpage.js +1 -1
- package/lib/ebook/pages/tocpage.js +26 -20
- package/lib/ebook/plantbook.js +6 -13
- package/lib/ebook/{image.js → taxonimage.js} +2 -2
- package/lib/exceptions.js +42 -26
- package/lib/externalsites.js +11 -4
- package/lib/families.js +10 -8
- package/lib/flowercolor.js +42 -0
- package/lib/genera.js +13 -21
- package/lib/genericpage.js +12 -0
- package/lib/html.js +11 -23
- package/lib/htmltaxon.js +89 -6
- package/lib/index.d.ts +54 -0
- package/lib/index.js +2 -30
- package/lib/jekyll.js +49 -21
- package/lib/jepson.js +7 -8
- package/lib/markdown.js +6 -0
- package/lib/pagerenderer.js +43 -8
- package/lib/plants/glossary.js +5 -0
- package/lib/program.js +47 -0
- package/lib/rareplants.js +44 -30
- package/lib/sitegenerator.js +41 -24
- package/lib/taxa.js +20 -134
- package/lib/taxon.js +1 -1
- package/lib/web/glossarypages.js +6 -0
- package/lib/web/pagetaxon.js +1 -5
- package/package.json +8 -7
- package/schemas/exceptions.schema.json +57 -0
- package/scripts/build-ebook.js +38 -47
- package/scripts/build-site.js +25 -14
- package/types/classes.d.ts +136 -8
- package/lib/commandandtaxaprocessor.js +0 -25
- package/lib/commandprocessor.js +0 -108
- package/lib/generictaxaloader.js +0 -48
- package/lib/taxaloader.js +0 -50
- package/lib/taxaprocessor.js +0 -34
- /package/data/text/{Calyptridium-ubellatum.md → Calyptridium-umbellatum.md} +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
Brown or black phyllaries.
|
@@ -0,0 +1 @@
|
|
1
|
+
Pink or white phyllaries.
|
@@ -0,0 +1 @@
|
|
1
|
+
Petals and sepals indistinguishable, usually with 5 veins, 20-40 mm long.
|
@@ -0,0 +1 @@
|
|
1
|
+
Petals and sepals indistinguishable, usually with 3 veins, 10-20 mm long.
|
@@ -0,0 +1 @@
|
|
1
|
+
Tall, often to 1.5 meters or occasionally taller. Lower part of stem leafless and coated with whitish powder.
|
@@ -0,0 +1 @@
|
|
1
|
+
Plant usually 50 cm or less in height.
|
@@ -0,0 +1 @@
|
|
1
|
+
Sepals at least as long as petals.
|
@@ -0,0 +1 @@
|
|
1
|
+
Petals longer than sepals.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves mostly basal and usually dissected into 3 parts; may have small undissected leaves along stem. Basal leaves with dense short hairs. May or may not have ray flowers.
|
@@ -0,0 +1 @@
|
|
1
|
+
Phyllary hairs glandular, phyllaries roughly equal in length. Leaves not dissected, generally 5-20 cm long, and much longer than they are wide.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves fuzzy.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves round. flower stems prostrate.
|
@@ -0,0 +1 @@
|
|
1
|
+
Leaves small and densely packed.
|
@@ -0,0 +1 @@
|
|
1
|
+
Small flowers (tube throat 4-8 mm).
|
@@ -0,0 +1 @@
|
|
1
|
+
Large flowers (tube throat 20-50 mm). Flowers light pink with dark purple stripes. Plant hairy.
|
@@ -0,0 +1 @@
|
|
1
|
+
Corolla throat yellow, sometimes with purple marks. Corolla throat 2-5 mm long.
|
@@ -0,0 +1 @@
|
|
1
|
+
Corolla throat purple, 6-8 mm long.
|
@@ -0,0 +1 @@
|
|
1
|
+
Terminal leaflet fused with adjacent leaflets. Preferred habitat is montane forest.
|
@@ -0,0 +1 @@
|
|
1
|
+
Terminal leaflet not fused with adjacent leaflets. Preferred habitat is rock talus.
|
@@ -0,0 +1 @@
|
|
1
|
+
Stem hairy and glandular.
|
@@ -0,0 +1 @@
|
|
1
|
+
Stem not hairy.
|
@@ -0,0 +1 @@
|
|
1
|
+
Petals with two lobes.
|
@@ -0,0 +1 @@
|
|
1
|
+
Flowers nodding. Petals with 4 lobes. Stamens and styles protruding significantly beyond petals.
|
@@ -0,0 +1 @@
|
|
1
|
+
Petals with 4 lobes; outer lobes pointed, smaller than inner lobes.
|
package/lib/basepagerenderer.js
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
import { Config } from "./config.js";
|
2
2
|
import { Files } from "./files.js";
|
3
3
|
import { Jekyll } from "./jekyll.js";
|
4
|
-
import { Taxa } from "./taxa.js";
|
5
4
|
import { GlossaryPages } from "./web/glossarypages.js";
|
6
5
|
|
7
6
|
class BasePageRenderer {
|
8
7
|
/**
|
9
8
|
* @param {string} outputDir
|
10
9
|
* @param {Taxa} taxa
|
11
|
-
* @param {
|
10
|
+
* @param {TaxaCol[]} [familyCols]
|
12
11
|
*/
|
13
|
-
static
|
12
|
+
static renderBasePages(outputDir, taxa, familyCols) {
|
14
13
|
const siteGenerator = new Jekyll(outputDir);
|
15
14
|
|
16
15
|
// Copy static files
|
@@ -20,7 +19,7 @@ class BasePageRenderer {
|
|
20
19
|
Files.copyDir("jekyll", outputDir);
|
21
20
|
|
22
21
|
// Copy illustrations.
|
23
|
-
siteGenerator.copyIllustrations(
|
22
|
+
siteGenerator.copyIllustrations(taxa.getFlowerColors());
|
24
23
|
|
25
24
|
taxa.getFamilies().renderPages(outputDir, familyCols);
|
26
25
|
|
package/lib/config.js
CHANGED
@@ -3,46 +3,69 @@ import * as url from "node:url";
|
|
3
3
|
import { Files } from "./files.js";
|
4
4
|
|
5
5
|
class Config {
|
6
|
+
static #packageDir = path.dirname(
|
7
|
+
path.dirname(url.fileURLToPath(import.meta.url))
|
8
|
+
);
|
6
9
|
|
7
|
-
|
8
|
-
|
10
|
+
/** @type {Object<string,Object<string,Object<string,{}>>>} */
|
9
11
|
#config = {};
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
+
/**
|
14
|
+
* @param {string} dataDir
|
15
|
+
*/
|
16
|
+
constructor(dataDir) {
|
17
|
+
this.#config = JSON.parse(Files.read(dataDir + "/config.json"));
|
13
18
|
}
|
14
19
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
/**
|
21
|
+
* @param {string} prefix
|
22
|
+
* @param {string} name
|
23
|
+
* @param {string|undefined} subcat
|
24
|
+
* @param {string} dflt
|
25
|
+
* @returns {string}
|
26
|
+
*/
|
27
|
+
getConfigValue(prefix, name, subcat, dflt) {
|
28
|
+
const obj = this.#config[prefix];
|
29
|
+
if (obj) {
|
30
|
+
if (Object.hasOwn(obj, name)) {
|
31
|
+
const nameObj = obj[name];
|
32
|
+
if (nameObj === undefined) {
|
21
33
|
return dflt;
|
22
34
|
}
|
23
|
-
if (
|
24
|
-
return nameObj;
|
35
|
+
if (subcat === undefined) {
|
36
|
+
return typeof nameObj === "string" ? nameObj : dflt;
|
25
37
|
}
|
26
|
-
if (
|
27
|
-
|
38
|
+
if (Object.hasOwn(nameObj, subcat)) {
|
39
|
+
const v = nameObj[subcat];
|
40
|
+
return typeof v === "string" ? v : dflt;
|
28
41
|
}
|
29
42
|
}
|
30
43
|
}
|
31
44
|
return dflt;
|
32
45
|
}
|
33
46
|
|
47
|
+
/**
|
48
|
+
* @returns {string[]}
|
49
|
+
*/
|
34
50
|
getCountyCodes() {
|
35
|
-
|
51
|
+
const counties = this.#config["counties"];
|
52
|
+
if (counties instanceof Array) {
|
53
|
+
return counties;
|
54
|
+
}
|
55
|
+
return [];
|
36
56
|
}
|
37
57
|
|
38
|
-
|
39
|
-
|
58
|
+
/**
|
59
|
+
* @param {string} name
|
60
|
+
* @param {string} dflt
|
61
|
+
*/
|
62
|
+
getLabel(name, dflt) {
|
63
|
+
return this.getConfigValue("labels", name, undefined, dflt);
|
40
64
|
}
|
41
65
|
|
42
66
|
static getPackageDir() {
|
43
67
|
return this.#packageDir;
|
44
68
|
}
|
45
|
-
|
46
69
|
}
|
47
70
|
|
48
|
-
export { Config };
|
71
|
+
export { Config };
|
package/lib/csv.js
CHANGED
@@ -5,81 +5,99 @@ import { parse as parseSync } from "csv-parse/sync";
|
|
5
5
|
import { parse } from "csv-parse";
|
6
6
|
|
7
7
|
class CSV {
|
8
|
+
/**
|
9
|
+
* @param {string} dir
|
10
|
+
* @param {string} fileName
|
11
|
+
*/
|
12
|
+
static getMap(dir, fileName) {
|
13
|
+
/** @type {string[]} */
|
14
|
+
let headers = [];
|
8
15
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
function setHeaders( h ) {
|
16
|
+
/**
|
17
|
+
* @param {string[]} h
|
18
|
+
*/
|
19
|
+
function setHeaders(h) {
|
14
20
|
headers = h;
|
15
21
|
return h;
|
16
22
|
}
|
17
23
|
|
18
|
-
const csv = this.parseFile(
|
24
|
+
const csv = this.parseFile(dir, fileName, setHeaders);
|
25
|
+
/** @type {Object<string,string[]>} */
|
19
26
|
const map = {};
|
20
|
-
for (
|
21
|
-
map[
|
27
|
+
for (const row of csv) {
|
28
|
+
map[row[headers[0]]] = row[headers[1]];
|
22
29
|
}
|
23
30
|
|
24
31
|
return map;
|
25
32
|
}
|
26
33
|
|
27
|
-
|
34
|
+
/**
|
35
|
+
* @param {string} fileName
|
36
|
+
* @param {import("csv-parse").ColumnOption[]|boolean|function (string[]):string[]} columns
|
37
|
+
* @param {string|undefined} delimiter
|
38
|
+
*/
|
39
|
+
static #getOptions(fileName, columns, delimiter) {
|
40
|
+
/** @type {import("csv-parse").Options} */
|
28
41
|
const options = { relax_column_count_less: true };
|
29
42
|
options.columns = columns;
|
30
|
-
if (
|
43
|
+
if (path.extname(fileName) === ".tsv") {
|
31
44
|
options.delimiter = "\t";
|
32
45
|
options.quote = false;
|
33
46
|
} else {
|
34
47
|
options.delimiter = delimiter ? delimiter : ",";
|
35
48
|
}
|
36
|
-
if (
|
49
|
+
if (options.delimiter === "\t") {
|
37
50
|
options.quote = null;
|
38
51
|
}
|
39
52
|
return options;
|
40
53
|
}
|
41
54
|
|
42
55
|
/**
|
43
|
-
* @param {string} dir
|
44
|
-
* @param {string} fileName
|
45
|
-
* @param {boolean|
|
46
|
-
* @param {string
|
56
|
+
* @param {string} dir
|
57
|
+
* @param {string} fileName
|
58
|
+
* @param {boolean|import("csv-parse").ColumnOption[]|function (string[]):string[]} [columns]
|
59
|
+
* @param {string} [delimiter]
|
47
60
|
*/
|
48
|
-
static parseFile(
|
49
|
-
const content = fs.readFileSync(
|
61
|
+
static parseFile(dir, fileName, columns = true, delimiter) {
|
62
|
+
const content = fs.readFileSync(dir + "/" + fileName);
|
50
63
|
|
51
|
-
const options = this.#getOptions(
|
52
|
-
return parseSync(
|
64
|
+
const options = this.#getOptions(fileName, columns, delimiter);
|
65
|
+
return parseSync(content, options);
|
53
66
|
}
|
54
67
|
|
55
68
|
/**
|
56
|
-
* @param {string} dir
|
57
|
-
* @param {string} fileName
|
58
|
-
* @param {boolean|
|
59
|
-
* @param {string|undefined} delimiter
|
60
|
-
* @param {
|
69
|
+
* @param {string} dir
|
70
|
+
* @param {string} fileName
|
71
|
+
* @param {boolean|import("csv-parse").ColumnOption[]} columns
|
72
|
+
* @param {string|undefined} delimiter
|
73
|
+
* @param {function (any):void} callback
|
61
74
|
*/
|
62
|
-
static async parseStream(
|
63
|
-
|
75
|
+
static async parseStream(
|
76
|
+
dir,
|
77
|
+
fileName,
|
78
|
+
columns = true,
|
79
|
+
delimiter,
|
80
|
+
callback
|
81
|
+
) {
|
82
|
+
const options = this.#getOptions(fileName, columns, delimiter);
|
64
83
|
const processFile = async () => {
|
84
|
+
/** @type {string[][]} */
|
65
85
|
const records = [];
|
66
86
|
const parser = fs
|
67
|
-
.createReadStream(
|
68
|
-
.pipe(
|
69
|
-
parser.on(
|
87
|
+
.createReadStream(dir + "/" + fileName)
|
88
|
+
.pipe(parse(options));
|
89
|
+
parser.on("readable", function () {
|
70
90
|
let record;
|
71
|
-
while (
|
72
|
-
callback(
|
91
|
+
while ((record = parser.read()) !== null) {
|
92
|
+
callback(record);
|
73
93
|
}
|
74
|
-
}
|
75
|
-
await finished(
|
94
|
+
});
|
95
|
+
await finished(parser);
|
76
96
|
return records;
|
77
97
|
};
|
78
98
|
// Parse the CSV content
|
79
99
|
await processFile();
|
80
|
-
|
81
100
|
}
|
82
|
-
|
83
101
|
}
|
84
102
|
|
85
|
-
export { CSV };
|
103
|
+
export { CSV };
|
package/lib/ebook/ebook.js
CHANGED
@@ -9,34 +9,40 @@ const MEDIA_TYPES = {
|
|
9
9
|
XHTML: "application/xhtml+xml",
|
10
10
|
};
|
11
11
|
|
12
|
+
/**
|
13
|
+
* @type {Object<string,string>}
|
14
|
+
*/
|
12
15
|
const EXT_MEDIA_MAP = {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
jpeg: MEDIA_TYPES.JPG,
|
17
|
+
jpg: MEDIA_TYPES.JPG,
|
18
|
+
svg: MEDIA_TYPES.SVG,
|
16
19
|
};
|
17
20
|
|
18
21
|
class EBook {
|
19
|
-
|
20
22
|
#outputDir;
|
21
23
|
#filename;
|
22
24
|
#pub_id;
|
23
25
|
#title;
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
+
/**
|
28
|
+
*
|
29
|
+
* @param {string} outputDir
|
30
|
+
* @param {string} filename
|
31
|
+
* @param {string} pub_id
|
32
|
+
* @param {string} title
|
33
|
+
*/
|
34
|
+
constructor(outputDir, filename, pub_id, title) {
|
27
35
|
this.#outputDir = outputDir;
|
28
36
|
this.#filename = filename;
|
29
37
|
this.#pub_id = pub_id;
|
30
38
|
this.#title = title;
|
31
39
|
|
32
40
|
// Initialize output directory.
|
33
|
-
fs.rmSync(
|
34
|
-
fs.mkdirSync(
|
35
|
-
|
41
|
+
fs.rmSync(this.#outputDir, { force: true, recursive: true });
|
42
|
+
fs.mkdirSync(this.getContentDir(), { recursive: true });
|
36
43
|
}
|
37
44
|
|
38
45
|
async create() {
|
39
|
-
|
40
46
|
const contentDir = this.getContentDir();
|
41
47
|
|
42
48
|
this.#createContainerFile();
|
@@ -44,36 +50,35 @@ class EBook {
|
|
44
50
|
this.#createPackageFile();
|
45
51
|
|
46
52
|
// Copy assets
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
fs.cpSync(Config.getPackageDir() + "/ebook", contentDir, {
|
54
|
+
recursive: true,
|
55
|
+
});
|
50
56
|
|
51
57
|
this.createZip();
|
52
|
-
|
53
58
|
}
|
54
59
|
|
55
60
|
#createContainerFile() {
|
56
|
-
|
57
61
|
const metaDir = this.#getMetaDir();
|
58
62
|
|
59
|
-
fs.mkdirSync(
|
63
|
+
fs.mkdirSync(metaDir, { recursive: true });
|
60
64
|
|
61
|
-
let xml =
|
62
|
-
|
65
|
+
let xml =
|
66
|
+
'<?xml version="1.0"?>' +
|
67
|
+
'<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">';
|
63
68
|
|
64
|
-
xml +=
|
69
|
+
xml +=
|
70
|
+
'<rootfiles><rootfile full-path="epub/package.opf" media-type="application/oebps-package+xml" /></rootfiles>';
|
65
71
|
xml += "</container>";
|
66
72
|
|
67
|
-
fs.writeFileSync(
|
68
|
-
|
73
|
+
fs.writeFileSync(metaDir + "/container.xml", xml);
|
69
74
|
}
|
70
75
|
|
71
76
|
#createPackageFile() {
|
72
|
-
|
73
77
|
const dir = this.getContentDir();
|
74
78
|
|
75
|
-
let xml =
|
76
|
-
|
79
|
+
let xml =
|
80
|
+
'<?xml version="1.0"?>\n' +
|
81
|
+
'<package version="3.0" xml:lang="en" xmlns="http://www.idpf.org/2007/opf" unique-identifier="pub-id">';
|
77
82
|
|
78
83
|
xml += this.#renderMetadata();
|
79
84
|
xml += this.#renderManifest();
|
@@ -81,50 +86,67 @@ class EBook {
|
|
81
86
|
|
82
87
|
xml += "</package>";
|
83
88
|
|
84
|
-
fs.writeFileSync(
|
85
|
-
|
89
|
+
fs.writeFileSync(dir + "/package.opf", xml);
|
86
90
|
}
|
87
91
|
|
88
92
|
async createPages() {
|
89
|
-
throw new Error(
|
93
|
+
throw new Error("must be implemented by subclass");
|
90
94
|
}
|
91
95
|
|
92
96
|
createZip() {
|
93
97
|
// Create zip.
|
94
98
|
const filename = this.#outputDir + "/" + this.#filename + ".epub";
|
95
|
-
const output = fs.createWriteStream(
|
96
|
-
const archive = archiver(
|
97
|
-
|
98
|
-
|
99
|
-
zlib: { level: 9 } // Sets the compression level.
|
100
|
-
}
|
101
|
-
);
|
99
|
+
const output = fs.createWriteStream(filename);
|
100
|
+
const archive = archiver("zip", {
|
101
|
+
zlib: { level: 9 }, // Sets the compression level.
|
102
|
+
});
|
102
103
|
|
103
|
-
archive.pipe(
|
104
|
+
archive.pipe(output);
|
104
105
|
|
105
|
-
archive.append(
|
106
|
+
archive.append("application/epub+zip", {
|
107
|
+
name: "mimetype",
|
108
|
+
store: true,
|
109
|
+
});
|
106
110
|
|
107
|
-
archive.directory(
|
108
|
-
archive.directory(
|
111
|
+
archive.directory(this.#getMetaDir(), "META-INF");
|
112
|
+
archive.directory(this.getContentDir(), "epub");
|
109
113
|
|
110
114
|
archive.finalize();
|
111
|
-
|
112
115
|
}
|
113
116
|
|
114
117
|
getContentDir() {
|
115
118
|
return this.#outputDir + "/epub";
|
116
119
|
}
|
117
120
|
|
118
|
-
|
119
|
-
|
121
|
+
/**
|
122
|
+
* @param {string} id
|
123
|
+
* @param {string} href
|
124
|
+
* @param {string} mediaType
|
125
|
+
*/
|
126
|
+
static getManifestEntry(id, href, mediaType = MEDIA_TYPES.XHTML) {
|
127
|
+
return (
|
128
|
+
'<item id="' +
|
129
|
+
id +
|
130
|
+
'" href="' +
|
131
|
+
href +
|
132
|
+
'" media-type="' +
|
133
|
+
mediaType +
|
134
|
+
'" />'
|
135
|
+
);
|
120
136
|
}
|
121
137
|
|
122
|
-
|
123
|
-
|
138
|
+
/**
|
139
|
+
* @param {string} ext
|
140
|
+
*/
|
141
|
+
static getMediaTypeForExt(ext) {
|
142
|
+
return EXT_MEDIA_MAP[ext];
|
124
143
|
}
|
125
144
|
|
126
|
-
|
127
|
-
|
145
|
+
/**
|
146
|
+
* @param {string} id
|
147
|
+
*/
|
148
|
+
static getSpineEntry(id) {
|
149
|
+
return '<itemref idref="' + id + '"/>';
|
128
150
|
}
|
129
151
|
|
130
152
|
#getMetaDir() {
|
@@ -133,38 +155,43 @@ class EBook {
|
|
133
155
|
|
134
156
|
#renderManifest() {
|
135
157
|
let xml = "<manifest>";
|
136
|
-
xml +=
|
158
|
+
xml += '<item id="c0" href="css/main.css" media-type="text/css" />';
|
137
159
|
xml += this.renderManifestEntries();
|
138
|
-
xml +=
|
160
|
+
xml +=
|
161
|
+
'<item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />';
|
139
162
|
return xml + "</manifest>";
|
140
163
|
}
|
141
164
|
|
142
165
|
renderManifestEntries() {
|
143
|
-
throw new Error(
|
166
|
+
throw new Error("must be implemented by subclass");
|
144
167
|
}
|
145
168
|
|
146
169
|
#renderMetadata() {
|
147
|
-
let xml =
|
148
|
-
xml += XHTML.textElement(
|
170
|
+
let xml = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">';
|
171
|
+
xml += XHTML.textElement("dc:identifier", this.#pub_id, {
|
172
|
+
id: "pub-id",
|
173
|
+
});
|
149
174
|
xml += "<dc:language>en-US</dc:language>";
|
150
|
-
xml += XHTML.textElement(
|
175
|
+
xml += XHTML.textElement("dc:title", this.#title);
|
151
176
|
const d = new Date();
|
152
|
-
d.setUTCMilliseconds(
|
153
|
-
xml +=
|
177
|
+
d.setUTCMilliseconds(0);
|
178
|
+
xml +=
|
179
|
+
'<meta property="dcterms:modified">' +
|
180
|
+
d.toISOString().replace(".000", "") +
|
181
|
+
"</meta>";
|
154
182
|
return xml + "</metadata>";
|
155
183
|
}
|
156
184
|
|
157
185
|
#renderSpine() {
|
158
186
|
let xml = "<spine>";
|
159
|
-
xml +=
|
187
|
+
xml += '<itemref idref="toc"/>';
|
160
188
|
xml += this.renderSpineElements();
|
161
189
|
return xml + "</spine>";
|
162
190
|
}
|
163
191
|
|
164
192
|
renderSpineElements() {
|
165
|
-
throw new Error(
|
193
|
+
throw new Error("must be implemented by subclass");
|
166
194
|
}
|
167
|
-
|
168
195
|
}
|
169
196
|
|
170
|
-
export { EBook };
|
197
|
+
export { EBook };
|
package/lib/ebook/ebookpage.js
CHANGED
@@ -1,22 +1,27 @@
|
|
1
1
|
import * as fs from "node:fs";
|
2
2
|
|
3
3
|
class EBookPage {
|
4
|
-
|
5
4
|
#fileName;
|
6
5
|
#title;
|
7
6
|
#rootPrefix;
|
8
7
|
|
9
|
-
|
8
|
+
/**
|
9
|
+
*
|
10
|
+
* @param {string} fileName
|
11
|
+
* @param {string} title
|
12
|
+
* @param {string} rootPrefix
|
13
|
+
*/
|
14
|
+
constructor(fileName, title, rootPrefix = "") {
|
10
15
|
this.#fileName = fileName;
|
11
16
|
this.#title = title;
|
12
17
|
this.#rootPrefix = rootPrefix;
|
13
18
|
}
|
14
19
|
|
15
20
|
create() {
|
16
|
-
let html = this.#renderPageStart(
|
21
|
+
let html = this.#renderPageStart(this.#title);
|
17
22
|
html += this.renderPageBody();
|
18
23
|
html += this.#renderPageEnd();
|
19
|
-
fs.writeFileSync(
|
24
|
+
fs.writeFileSync(this.#fileName, html);
|
20
25
|
}
|
21
26
|
|
22
27
|
getTitle() {
|
@@ -28,22 +33,28 @@ class EBookPage {
|
|
28
33
|
}
|
29
34
|
|
30
35
|
renderPageBody() {
|
31
|
-
throw new Error(
|
36
|
+
throw new Error("must be implemented by subclass");
|
32
37
|
}
|
33
38
|
|
34
39
|
#renderPageEnd() {
|
35
40
|
return "</body></html>";
|
36
41
|
}
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
/**
|
44
|
+
* @param {string} title
|
45
|
+
*/
|
46
|
+
#renderPageStart(title) {
|
47
|
+
let html = '<?xml version="1.0" encoding="utf-8"?>\n';
|
48
|
+
html +=
|
49
|
+
'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">';
|
41
50
|
html += "<head><title>" + title + "</title>";
|
42
|
-
html +=
|
51
|
+
html +=
|
52
|
+
'<link href="' +
|
53
|
+
this.#rootPrefix +
|
54
|
+
'css/main.css" rel="stylesheet" />';
|
43
55
|
html += "</head>" + this.#renderBodyStart();
|
44
56
|
return html;
|
45
57
|
}
|
46
|
-
|
47
58
|
}
|
48
59
|
|
49
|
-
export { EBookPage };
|
60
|
+
export { EBookPage };
|