@ca-plant-list/ca-plant-list 0.1.22 → 0.2.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/data/config.json +16 -0
- package/data/exceptions.json +1 -0
- package/data/photos.csv +6 -0
- package/data/synonyms.csv +1970 -0
- package/data/taxa.csv +1828 -0
- package/data/text/Leptosiphon-bicolor.md +1 -0
- package/data/text/Leptosiphon-ciliatus.md +1 -0
- package/data/text/Leptosiphon-grandiflorus.md +1 -0
- package/data/text/Lupinus-bicolor.md +1 -0
- package/data/text/Lupinus-nanus.md +1 -0
- package/ebook/css/main.css +59 -0
- package/lib/dataloader.js +19 -1
- package/lib/ebook/ebook.js +146 -0
- package/lib/ebook/ebookpage.js +43 -0
- package/lib/ebook/image.js +21 -0
- package/lib/ebook/pages/taxonpage.js +77 -0
- package/lib/ebook/pages/tocpage.js +30 -0
- package/lib/ebook/plantbook.js +122 -0
- package/lib/ebook/xhtml.js +7 -0
- package/lib/files.js +4 -0
- package/lib/genera.js +1 -1
- package/lib/html.js +5 -2
- package/lib/index.d.ts +192 -0
- package/lib/index.js +4 -3
- package/lib/taxa.js +49 -34
- package/lib/taxon.js +26 -8
- package/package.json +8 -2
- package/scripts/build-ebook.js +12 -0
- package/{build-site.js → scripts/build-site.js} +3 -3
- package/tsconfig.json +0 -14
@@ -0,0 +1 @@
|
|
1
|
+
Flowers pedicelled. Corolla tube at least twice as long as calyx. Calyx membrane much narrower than lobes.
|
@@ -0,0 +1 @@
|
|
1
|
+
Flowers pedicelled. Corolla tube at least twice as long as calyx. Calyx membrane at least as wide as lobes.
|
@@ -0,0 +1 @@
|
|
1
|
+
Flowers sessile.
|
@@ -0,0 +1 @@
|
|
1
|
+
Banner longer than wide; pedicels generally < 3 mm.
|
@@ -0,0 +1 @@
|
|
1
|
+
Banner as wide as or wider than long; pedicels generally > 3 mm.
|
@@ -0,0 +1,59 @@
|
|
1
|
+
img {
|
2
|
+
width: 100%;
|
3
|
+
}
|
4
|
+
|
5
|
+
figure {
|
6
|
+
page-break-inside: avoid;
|
7
|
+
}
|
8
|
+
|
9
|
+
span.color {
|
10
|
+
border: solid black 1px;
|
11
|
+
border-radius: .25rem;
|
12
|
+
margin-right: .5rem;
|
13
|
+
padding: .25rem;
|
14
|
+
}
|
15
|
+
|
16
|
+
span.color.blue {
|
17
|
+
background-color: blue;
|
18
|
+
color: white;
|
19
|
+
}
|
20
|
+
|
21
|
+
span.color.pink {
|
22
|
+
background-color: pink;
|
23
|
+
color: white;
|
24
|
+
}
|
25
|
+
|
26
|
+
span.color.red {
|
27
|
+
background-color: red;
|
28
|
+
color: white;
|
29
|
+
}
|
30
|
+
|
31
|
+
span.color.yellow {
|
32
|
+
background-color: yellow;
|
33
|
+
color: black;
|
34
|
+
}
|
35
|
+
|
36
|
+
span.color.white {
|
37
|
+
background-color: white;
|
38
|
+
color: black;
|
39
|
+
}
|
40
|
+
|
41
|
+
span.color.blue::after {
|
42
|
+
content: "Blue"
|
43
|
+
}
|
44
|
+
|
45
|
+
span.color.pink::after {
|
46
|
+
content: "Pink"
|
47
|
+
}
|
48
|
+
|
49
|
+
span.color.red::after {
|
50
|
+
content: "Red"
|
51
|
+
}
|
52
|
+
|
53
|
+
span.color.white::after {
|
54
|
+
content: "White"
|
55
|
+
}
|
56
|
+
|
57
|
+
span.color.yellow::after {
|
58
|
+
content: "Yellow"
|
59
|
+
}
|
package/lib/dataloader.js
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
import { CSV } from "./csv.js";
|
1
2
|
import { Taxa } from "./taxa.js";
|
2
3
|
import { Families } from "./families.js";
|
3
4
|
import { Exceptions } from "./exceptions.js";
|
5
|
+
import { Files } from "./files.js";
|
4
6
|
|
5
7
|
const OPTION_DEFS = [
|
6
8
|
{ name: "datadir", type: String, defaultValue: "./data" },
|
@@ -19,12 +21,28 @@ class DataLoader {
|
|
19
21
|
|
20
22
|
static load( options ) {
|
21
23
|
|
24
|
+
function getIncludeList() {
|
25
|
+
// Read inclusion list.
|
26
|
+
const includeFileName = "taxa_include.csv";
|
27
|
+
if ( !Files.exists( taxaDir + "/" + includeFileName ) ) {
|
28
|
+
console.log( includeFileName + " not found; loading all taxa" );
|
29
|
+
return true;
|
30
|
+
}
|
31
|
+
const includeCSV = CSV.parseFile( taxaDir, includeFileName );
|
32
|
+
const include = {};
|
33
|
+
for ( const row of includeCSV ) {
|
34
|
+
include[ row[ "taxon_name" ] ] = row;
|
35
|
+
}
|
36
|
+
return include;
|
37
|
+
}
|
38
|
+
|
22
39
|
const taxaDir = options.datadir;
|
23
40
|
|
24
41
|
console.log( "loading data" );
|
25
42
|
|
26
43
|
this.init( taxaDir );
|
27
|
-
|
44
|
+
|
45
|
+
Taxa.init( getIncludeList() );
|
28
46
|
|
29
47
|
}
|
30
48
|
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import * as fs from "node:fs";
|
2
|
+
import { default as archiver } from "archiver";
|
3
|
+
import { XHTML } from "./xhtml.js";
|
4
|
+
import { Config } from "../config.js";
|
5
|
+
|
6
|
+
class EBook {
|
7
|
+
|
8
|
+
#outputDir;
|
9
|
+
#filename;
|
10
|
+
#pub_id;
|
11
|
+
#title;
|
12
|
+
|
13
|
+
constructor( outputDir, filename, pub_id, title ) {
|
14
|
+
|
15
|
+
this.#outputDir = outputDir;
|
16
|
+
this.#filename = filename;
|
17
|
+
this.#pub_id = pub_id;
|
18
|
+
this.#title = title;
|
19
|
+
|
20
|
+
// Initialize output directory.
|
21
|
+
fs.rmSync( this.#outputDir, { force: true, recursive: true } );
|
22
|
+
fs.mkdirSync( this.getContentDir(), { recursive: true } );
|
23
|
+
|
24
|
+
}
|
25
|
+
|
26
|
+
async create() {
|
27
|
+
|
28
|
+
const contentDir = this.getContentDir();
|
29
|
+
|
30
|
+
this.#createContainerFile();
|
31
|
+
await this.createPages();
|
32
|
+
this.#createPackageFile();
|
33
|
+
|
34
|
+
// Copy assets
|
35
|
+
const cssDirTarget = contentDir + "/css";
|
36
|
+
fs.mkdirSync( cssDirTarget, { recursive: true } );
|
37
|
+
fs.cpSync( Config.getPackageDir() + "/ebook/css", cssDirTarget, { recursive: true } );
|
38
|
+
|
39
|
+
this.createZip();
|
40
|
+
|
41
|
+
}
|
42
|
+
|
43
|
+
#createContainerFile() {
|
44
|
+
|
45
|
+
const metaDir = this.#getMetaDir();
|
46
|
+
|
47
|
+
fs.mkdirSync( metaDir, { recursive: true } );
|
48
|
+
|
49
|
+
let xml = "<?xml version=\"1.0\"?>"
|
50
|
+
+ "<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">";
|
51
|
+
|
52
|
+
xml += "<rootfiles><rootfile full-path=\"epub/package.opf\" media-type=\"application/oebps-package+xml\" /></rootfiles>";
|
53
|
+
xml += "</container>";
|
54
|
+
|
55
|
+
fs.writeFileSync( metaDir + "/container.xml", xml );
|
56
|
+
|
57
|
+
}
|
58
|
+
|
59
|
+
#createPackageFile() {
|
60
|
+
|
61
|
+
const dir = this.getContentDir();
|
62
|
+
|
63
|
+
let xml = "<?xml version=\"1.0\"?>\n"
|
64
|
+
+ "<package version=\"3.0\" xml:lang=\"en\" xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"pub-id\">";
|
65
|
+
|
66
|
+
xml += this.#renderMetadata();
|
67
|
+
xml += this.#renderManifest();
|
68
|
+
xml += this.#renderSpine();
|
69
|
+
|
70
|
+
xml += "</package>";
|
71
|
+
|
72
|
+
fs.writeFileSync( dir + "/package.opf", xml );
|
73
|
+
|
74
|
+
}
|
75
|
+
|
76
|
+
async createPages() {
|
77
|
+
throw new Error( "must be implemented by subclass" );
|
78
|
+
}
|
79
|
+
|
80
|
+
createZip() {
|
81
|
+
// Create zip.
|
82
|
+
const filename = this.#outputDir + "/" + this.#filename + ".epub";
|
83
|
+
const output = fs.createWriteStream( filename );
|
84
|
+
const archive = archiver(
|
85
|
+
"zip",
|
86
|
+
{
|
87
|
+
zlib: { level: 9 } // Sets the compression level.
|
88
|
+
}
|
89
|
+
);
|
90
|
+
|
91
|
+
archive.pipe( output );
|
92
|
+
|
93
|
+
archive.append( "application/epub+zip", { name: "mimetype", store: true } );
|
94
|
+
|
95
|
+
archive.directory( this.#getMetaDir(), "META-INF" );
|
96
|
+
archive.directory( this.getContentDir(), "epub" );
|
97
|
+
|
98
|
+
archive.finalize();
|
99
|
+
|
100
|
+
}
|
101
|
+
|
102
|
+
getContentDir() {
|
103
|
+
return this.#outputDir + "/epub";
|
104
|
+
}
|
105
|
+
|
106
|
+
#getMetaDir() {
|
107
|
+
return this.#outputDir + "/META-INF";
|
108
|
+
}
|
109
|
+
|
110
|
+
#renderManifest() {
|
111
|
+
let xml = "<manifest>";
|
112
|
+
xml += "<item id=\"c0\" href=\"css/main.css\" media-type=\"text/css\" />";
|
113
|
+
xml += this.renderManifestEntries();
|
114
|
+
xml += "<item id=\"toc\" href=\"toc.xhtml\" media-type=\"application/xhtml+xml\" properties=\"nav\" />";
|
115
|
+
return xml + "</manifest>";
|
116
|
+
}
|
117
|
+
|
118
|
+
renderManifestEntries() {
|
119
|
+
throw new Error( "must be implemented by subclass" );
|
120
|
+
}
|
121
|
+
|
122
|
+
#renderMetadata() {
|
123
|
+
let xml = "<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">";
|
124
|
+
xml += XHTML.textElement( "dc:identifier", this.#pub_id, { id: "pub-id" } );
|
125
|
+
xml += "<dc:language>en-US</dc:language>";
|
126
|
+
xml += XHTML.textElement( "dc:title", this.#title );
|
127
|
+
const d = new Date();
|
128
|
+
d.setUTCMilliseconds( 0 );
|
129
|
+
xml += "<meta property=\"dcterms:modified\">" + d.toISOString().replace( ".000", "" ) + "</meta>";
|
130
|
+
return xml + "</metadata>";
|
131
|
+
}
|
132
|
+
|
133
|
+
#renderSpine() {
|
134
|
+
let xml = "<spine>";
|
135
|
+
xml += "<itemref idref=\"toc\"/>";
|
136
|
+
xml += this.renderSpineElements();
|
137
|
+
return xml + "</spine>";
|
138
|
+
}
|
139
|
+
|
140
|
+
renderSpineElements() {
|
141
|
+
throw new Error( "must be implemented by subclass" );
|
142
|
+
}
|
143
|
+
|
144
|
+
}
|
145
|
+
|
146
|
+
export { EBook };
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import * as fs from "node:fs";
|
2
|
+
|
3
|
+
class EBookPage {
|
4
|
+
|
5
|
+
#fileName;
|
6
|
+
#title;
|
7
|
+
|
8
|
+
constructor( fileName, title ) {
|
9
|
+
this.#fileName = fileName;
|
10
|
+
this.#title = title;
|
11
|
+
}
|
12
|
+
|
13
|
+
create() {
|
14
|
+
let html = this.#renderPageStart( this.#title );
|
15
|
+
html += this.renderPageBody();
|
16
|
+
html += this.#renderPageEnd();
|
17
|
+
fs.writeFileSync( this.#fileName, html );
|
18
|
+
}
|
19
|
+
|
20
|
+
#renderBodyStart() {
|
21
|
+
return "<body>";
|
22
|
+
}
|
23
|
+
|
24
|
+
renderPageBody() {
|
25
|
+
throw new Error( "must be implemented by subclass" );
|
26
|
+
}
|
27
|
+
|
28
|
+
#renderPageEnd() {
|
29
|
+
return "</body></html>";
|
30
|
+
}
|
31
|
+
|
32
|
+
#renderPageStart( title ) {
|
33
|
+
let html = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
34
|
+
html += "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">";
|
35
|
+
html += "<head><title>" + title + "</title>";
|
36
|
+
html += "<link href=\"./css/main.css\" rel=\"stylesheet\" />";
|
37
|
+
html += "</head>" + this.#renderBodyStart();
|
38
|
+
return html;
|
39
|
+
}
|
40
|
+
|
41
|
+
}
|
42
|
+
|
43
|
+
export { EBookPage };
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Image {
|
2
|
+
|
3
|
+
#src;
|
4
|
+
#credit;
|
5
|
+
|
6
|
+
constructor( src, credit ) {
|
7
|
+
this.#src = src;
|
8
|
+
this.#credit = credit;
|
9
|
+
}
|
10
|
+
|
11
|
+
getCaption() {
|
12
|
+
return this.#credit ? this.#credit : undefined;
|
13
|
+
}
|
14
|
+
|
15
|
+
getSrc() {
|
16
|
+
return this.#src;
|
17
|
+
}
|
18
|
+
|
19
|
+
}
|
20
|
+
|
21
|
+
export { Image };
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import sizeOf from "image-size";
|
2
|
+
import markdownIt from "markdown-it";
|
3
|
+
import { Config, Files } from "@ca-plant-list/ca-plant-list";
|
4
|
+
import { EBookPage } from "../ebookpage.js";
|
5
|
+
import { XHTML } from "../xhtml.js";
|
6
|
+
|
7
|
+
class TaxonPage extends EBookPage {
|
8
|
+
|
9
|
+
#outputDir;
|
10
|
+
#taxon;
|
11
|
+
#photos;
|
12
|
+
|
13
|
+
constructor( outputDir, taxon, photos ) {
|
14
|
+
super( outputDir + "/" + taxon.getFileName(), taxon.getName() );
|
15
|
+
this.#outputDir = outputDir;
|
16
|
+
this.#taxon = taxon;
|
17
|
+
this.#photos = photos;
|
18
|
+
}
|
19
|
+
|
20
|
+
renderPageBody() {
|
21
|
+
|
22
|
+
function renderColors( colors ) {
|
23
|
+
if ( !colors ) {
|
24
|
+
return "";
|
25
|
+
}
|
26
|
+
let html = "";
|
27
|
+
for ( const color of colors ) {
|
28
|
+
html += XHTML.textElement( "span", "", { class: "color " + color } );
|
29
|
+
}
|
30
|
+
return html;
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
function renderCustomText( name ) {
|
35
|
+
// See if there is custom text.
|
36
|
+
const fileName = Config.getPackageDir() + "/data/text/" + name + ".md";
|
37
|
+
if ( !Files.exists( fileName ) ) {
|
38
|
+
return "";
|
39
|
+
}
|
40
|
+
const text = Files.read( fileName );
|
41
|
+
const md = new markdownIt();
|
42
|
+
return md.render( text );
|
43
|
+
}
|
44
|
+
|
45
|
+
const name = this.#taxon.getName();
|
46
|
+
let html = XHTML.textElement( "h1", name );
|
47
|
+
|
48
|
+
html += XHTML.textElement( "div", this.#taxon.getFamily().getName() );
|
49
|
+
|
50
|
+
const cn = this.#taxon.getCommonNames();
|
51
|
+
if ( cn && cn.length > 0 ) {
|
52
|
+
html += XHTML.textElement( "p", cn.join( ", " ) );
|
53
|
+
}
|
54
|
+
|
55
|
+
html += renderColors( this.#taxon.getFlowerColors() );
|
56
|
+
|
57
|
+
html += renderCustomText( this.#taxon.getBaseFileName() );
|
58
|
+
|
59
|
+
if ( this.#photos ) {
|
60
|
+
for ( const photo of this.#photos ) {
|
61
|
+
const src = photo.getSrc();
|
62
|
+
const dimensions = sizeOf( this.#outputDir + "/" + src );
|
63
|
+
let img = XHTML.textElement( "img", "", { src: src, style: "max-width:" + dimensions.width + "px" } );
|
64
|
+
const caption = photo.getCaption();
|
65
|
+
if ( caption ) {
|
66
|
+
img += XHTML.textElement( "figcaption", caption );
|
67
|
+
}
|
68
|
+
html += XHTML.wrap( "figure", img );
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
return html;
|
73
|
+
}
|
74
|
+
|
75
|
+
}
|
76
|
+
|
77
|
+
export { TaxonPage };
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { HTML } from "@ca-plant-list/ca-plant-list";
|
2
|
+
import { EBookPage } from "../ebookpage.js";
|
3
|
+
|
4
|
+
class TOCPage extends EBookPage {
|
5
|
+
|
6
|
+
#taxa;
|
7
|
+
|
8
|
+
constructor( outputDir, taxa ) {
|
9
|
+
super( outputDir + "/toc.xhtml", "Table of Contents" );
|
10
|
+
this.#taxa = taxa;
|
11
|
+
}
|
12
|
+
|
13
|
+
renderPageBody() {
|
14
|
+
|
15
|
+
let html = "<nav id=\"toc\" role=\"doc-toc\" epub:type=\"toc\">";
|
16
|
+
html += "<h2 epub:type=\"title\">Table of Contents</h2>";
|
17
|
+
|
18
|
+
html += "<ol>";
|
19
|
+
for ( const taxon of this.#taxa ) {
|
20
|
+
html += "<li>" + HTML.getLink( "./" + taxon.getFileName(), taxon.getName() ) + "</li>";
|
21
|
+
}
|
22
|
+
html += "</ol>";
|
23
|
+
|
24
|
+
html += "</nav>";
|
25
|
+
|
26
|
+
return html;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
export { TOCPage };
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import * as fs from "node:fs";
|
2
|
+
import path from "node:path";
|
3
|
+
import sharp from "sharp";
|
4
|
+
import { CSV, Families, Files, Taxa } from "@ca-plant-list/ca-plant-list";
|
5
|
+
import { EBook } from "./ebook.js";
|
6
|
+
import { Image } from "./image.js";
|
7
|
+
import { TaxonPage } from "./pages/taxonpage.js";
|
8
|
+
import { TOCPage } from "./pages/tocpage.js";
|
9
|
+
import { Config } from "../config.js";
|
10
|
+
|
11
|
+
class PlantBook extends EBook {
|
12
|
+
|
13
|
+
#images = {};
|
14
|
+
|
15
|
+
constructor() {
|
16
|
+
|
17
|
+
super(
|
18
|
+
"output",
|
19
|
+
Config.getConfigValue( "ebook", "filename" ),
|
20
|
+
Config.getConfigValue( "ebook", "pub_id" ),
|
21
|
+
Config.getConfigValue( "ebook", "title" )
|
22
|
+
);
|
23
|
+
|
24
|
+
Families.init();
|
25
|
+
|
26
|
+
}
|
27
|
+
|
28
|
+
async createPages() {
|
29
|
+
await this.#importImages();
|
30
|
+
|
31
|
+
const contentDir = this.getContentDir();
|
32
|
+
const taxa = Taxa.getTaxa();
|
33
|
+
for ( const taxon of taxa ) {
|
34
|
+
const name = taxon.getName();
|
35
|
+
new TaxonPage( contentDir, taxon, this.#images[ name ] ).create();
|
36
|
+
}
|
37
|
+
new TOCPage( contentDir, taxa ).create();
|
38
|
+
}
|
39
|
+
|
40
|
+
#getMapEntry( map, key, initialValue ) {
|
41
|
+
const value = map[ key ];
|
42
|
+
if ( value ) {
|
43
|
+
return value;
|
44
|
+
}
|
45
|
+
map[ key ] = initialValue;
|
46
|
+
return initialValue;
|
47
|
+
}
|
48
|
+
|
49
|
+
async #importImages() {
|
50
|
+
|
51
|
+
const photoDirSrc = "external_data/photos";
|
52
|
+
const imagePrefix = "i";
|
53
|
+
const photoDirTarget = this.getContentDir() + "/" + imagePrefix;
|
54
|
+
fs.mkdirSync( photoDirSrc, { recursive: true } );
|
55
|
+
fs.mkdirSync( photoDirTarget, { recursive: true } );
|
56
|
+
|
57
|
+
const rows = CSV.parseFile( Config.getPackageDir() + "/data", "photos.csv" );
|
58
|
+
for ( const row of rows ) {
|
59
|
+
|
60
|
+
const name = row[ "taxon_name" ];
|
61
|
+
const taxon = Taxa.getTaxon( name );
|
62
|
+
if ( !taxon ) {
|
63
|
+
continue;
|
64
|
+
}
|
65
|
+
|
66
|
+
let imageList = this.#images[ name ];
|
67
|
+
if ( !imageList ) {
|
68
|
+
imageList = [];
|
69
|
+
this.#images[ name ] = imageList;
|
70
|
+
}
|
71
|
+
|
72
|
+
const src = new URL( row[ "source" ] );
|
73
|
+
const parts = path.parse( src.pathname ).dir.split( "/" );
|
74
|
+
const prefix = src.host.includes( "calflora" ) ? "cf-" : "inat-";
|
75
|
+
const filename = prefix + parts.slice( -1 )[ 0 ] + ".jpg";
|
76
|
+
const srcFileName = photoDirSrc + "/" + filename;
|
77
|
+
const targetFileName = photoDirTarget + "/" + filename;
|
78
|
+
|
79
|
+
if ( !fs.existsSync( srcFileName ) ) {
|
80
|
+
// File is not there; retrieve it.
|
81
|
+
console.log( "retrieving " + srcFileName );
|
82
|
+
await Files.fetch( src, srcFileName );
|
83
|
+
}
|
84
|
+
|
85
|
+
await new sharp( srcFileName ).resize( { width: 400 } ).jpeg( { quality: 40 } ).toFile( targetFileName );
|
86
|
+
|
87
|
+
imageList.push( new Image( imagePrefix + "/" + filename, row[ "credit" ] ) );
|
88
|
+
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
renderManifestEntries() {
|
93
|
+
let xml = "";
|
94
|
+
|
95
|
+
// Add taxon pages.
|
96
|
+
const taxa = Taxa.getTaxa();
|
97
|
+
for ( let index = 0; index < taxa.length; index++ ) {
|
98
|
+
const taxon = taxa[ index ];
|
99
|
+
xml += "<item id=\"t" + index + "\" href=\"" + taxon.getFileName() + "\" media-type=\"application/xhtml+xml\" />";
|
100
|
+
}
|
101
|
+
|
102
|
+
// Add images.
|
103
|
+
let index = 0;
|
104
|
+
for ( const imageList of Object.values( this.#images ) ) {
|
105
|
+
for ( const image of imageList ) {
|
106
|
+
xml += "<item id=\"i" + index + "\" href=\"" + image.getSrc() + "\" media-type=\"image/jpeg\" />";
|
107
|
+
index++;
|
108
|
+
}
|
109
|
+
}
|
110
|
+
return xml;
|
111
|
+
}
|
112
|
+
|
113
|
+
renderSpineElements() {
|
114
|
+
let xml = "";
|
115
|
+
for ( let index = 0; index < Taxa.getTaxa().length; index++ ) {
|
116
|
+
xml += "<itemref idref=\"t" + index + "\"/>";
|
117
|
+
}
|
118
|
+
return xml;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
export { PlantBook };
|
package/lib/files.js
CHANGED
package/lib/genera.js
CHANGED
package/lib/html.js
CHANGED
@@ -41,8 +41,11 @@ export class HTML {
|
|
41
41
|
if ( escape && ( typeof text === "string" ) ) {
|
42
42
|
text = this.escapeText( text );
|
43
43
|
}
|
44
|
-
|
45
|
-
|
44
|
+
// If tag is empty, make it self-closing so it is XHTML (epub) compatible.
|
45
|
+
if ( text === "" ) {
|
46
|
+
return html + "/>";
|
47
|
+
}
|
48
|
+
return html + ">" + text + "</" + elName + ">";
|
46
49
|
}
|
47
50
|
|
48
51
|
/**
|