@ca-plant-list/ca-plant-list 0.4.33 → 0.4.36

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.
Files changed (79) hide show
  1. package/data/glossary/peduncle.md +1 -1
  2. package/data/inatobsphotos.csv +499 -75
  3. package/data/inattaxonphotos.csv +728 -242
  4. package/data/synonyms.csv +86 -0
  5. package/data/taxa.csv +1985 -1905
  6. package/data/text/Allium-obtusum-var-obtusum.md +1 -0
  7. package/data/text/Allium-validum.md +1 -0
  8. package/data/text/Amelanchier-alnifolia-var-pumila.md +1 -0
  9. package/data/text/Amelanchier-utahensis.md +1 -0
  10. package/data/text/Angelica-breweri.md +1 -0
  11. package/data/text/Angelica-capitellata.md +1 -0
  12. package/data/text/Castilleja-applegatei-subsp-pallida.md +1 -0
  13. package/data/text/Castilleja-miniata-subsp-miniata.md +1 -0
  14. package/data/text/Ceanothus-cordulatus.md +1 -0
  15. package/data/text/Ceanothus-velutinus.md +1 -0
  16. package/data/text/Collinsia-parviflora.md +1 -0
  17. package/data/text/Collinsia-torreyi.md +1 -0
  18. package/data/text/Ericameria-nauseosa-var-speciosa.md +1 -0
  19. package/data/text/Ericameria-nauseosa.md +1 -0
  20. package/data/text/Erigeron-algidus.md +1 -0
  21. package/data/text/Erigeron-coulteri.md +1 -0
  22. package/data/text/Hackelia-micrantha.md +1 -0
  23. package/data/text/Hieracium-albiflorum.md +1 -0
  24. package/data/text/Hieracium-horridum.md +1 -0
  25. package/data/text/Hieracium-triste.md +1 -0
  26. package/data/text/Linanthus-pungens-subsp-pulchriflorus.md +1 -0
  27. package/data/text/Lupinus-argenteus-var-meionanthus.md +1 -0
  28. package/data/text/Lupinus-latifolius-var-latifolius.md +1 -1
  29. package/data/text/Lupinus-lepidus-var-lobbii.md +1 -0
  30. package/data/text/Lupinus-polyphyllus-var-burkei.md +1 -0
  31. package/data/text/Pedicularis-attollens.md +1 -0
  32. package/data/text/Pedicularis-groenlandica.md +1 -0
  33. package/data/text/Penstemon-deustus-var-deustus.md +1 -0
  34. package/data/text/Penstemon-deustus-var-pedicellatus.md +1 -0
  35. package/data/text/Penstemon-heterodoxus-var-heterodoxus.md +1 -0
  36. package/data/text/Penstemon-rydbergii-var-oreocharis.md +1 -0
  37. package/data/text/Phlox-diffusa.md +1 -0
  38. package/data/text/Potentilla-flabellifolia.md +1 -0
  39. package/data/text/Potentilla-gracilis-var-fastigiata.md +1 -0
  40. package/data/text/Primula-jeffreyi.md +1 -1
  41. package/data/text/Primula-tetrandra.md +1 -1
  42. package/data/text/Pseudognaphalium-beneolens.md +1 -0
  43. package/data/text/Pseudognaphalium-californicum.md +1 -0
  44. package/data/text/Pseudognaphalium-ramosissimum.md +1 -0
  45. package/data/text/Pyrola-asarifolia-subsp-asarifolia.md +1 -0
  46. package/data/text/Pyrola-dentata.md +1 -0
  47. package/data/text/Pyrola-picta.md +1 -0
  48. package/data/text/Salix-lemmonii.md +1 -0
  49. package/data/text/Salix-scouleriana.md +1 -0
  50. package/data/text/Senecio-integerrimus-var-exaltatus.md +1 -0
  51. package/data/text/Senecio-integerrimus-var-major.md +1 -0
  52. package/data/text/Senecio-triangularis.md +1 -0
  53. package/data/text/Sorbus-californica.md +1 -0
  54. package/data/text/Sorbus-scopulina.md +1 -0
  55. package/ebook/css/main.css +20 -13
  56. package/lib/csv.js +0 -2
  57. package/lib/ebook/ebook.js +178 -178
  58. package/lib/ebook/pages/taxonpage.js +19 -9
  59. package/lib/ebook/plantbook.js +17 -3
  60. package/lib/externalsites.js +12 -0
  61. package/lib/htmltaxon.js +13 -2
  62. package/lib/index.d.ts +3 -0
  63. package/lib/markdown.js +1 -1
  64. package/lib/program.js +0 -26
  65. package/lib/sitegenerator.js +4 -4
  66. package/lib/taxonomy/taxa.js +26 -5
  67. package/lib/taxonomy/taxon.js +12 -2
  68. package/lib/tools/calflora.js +0 -2
  69. package/lib/tools/calipc.js +111 -0
  70. package/lib/tools/taxacsv.js +5 -4
  71. package/lib/utils/eleventyGenerator.js +2 -0
  72. package/lib/utils/inat-tools.js +8 -4
  73. package/lib/web/pageTaxon.js +1 -0
  74. package/package.json +6 -6
  75. package/scripts/build-ebook.js +21 -6
  76. package/scripts/build-site.js +1 -1
  77. package/scripts/cpl-photos.js +1 -1
  78. package/scripts/cpl-tools.js +15 -1
  79. package/scripts/inattaxonphotos.js +1 -1
@@ -0,0 +1 @@
1
+ Nearly prostrate.
@@ -0,0 +1 @@
1
+ Stamens protruding beyond petals.
@@ -0,0 +1 @@
1
+ Upper leaf without hairs. Flower with 5 [styles](./g/style.html).
@@ -0,0 +1 @@
1
+ Upper leaf with hairs. Flower with 2 to 4 [styles](./g/style.html).
@@ -0,0 +1 @@
1
+ Umbels hairless or sparsely hairy. Secondary umbels open, not spherical.
@@ -0,0 +1 @@
1
+ Umbels covered with hairs. Secondary umbels spherical.
@@ -0,0 +1 @@
1
+ In dry areas. Leaf usually with 3 lobes. Generally shorther than _C. miniata_.
@@ -0,0 +1 @@
1
+ In wet areas. Usually 50 cm or taller. Leaves not toothed or lobed.
@@ -0,0 +1 @@
1
+ Twigs grayish, stiff and thorn-like.
@@ -0,0 +1 @@
1
+ Twigs green or brown, flexible and not thorny.
@@ -0,0 +1 @@
1
+ Inflorescence hairless or sparsely glandular.
@@ -0,0 +1 @@
1
+ Inflorescence densely glandular.
@@ -0,0 +1 @@
1
+ Generally in dry areas. Disc flowers only. Phyllaries with keels.
@@ -0,0 +1 @@
1
+ Disc flowers only. Phyllaries with keels.
@@ -0,0 +1 @@
1
+ Leaves on stem sparse and narrow. Phyllaries and stem hairy.
@@ -0,0 +1 @@
1
+ Leaves on stem usually clasping the stem. Ray flowers narrow (narrower than _E. glacialis_).
@@ -0,0 +1 @@
1
+ [Calyx](./g/calyx.html) shorter than 3mm. Corolla tube does not extend much beyond calyx. Flowers usually 5-8mm in diameter.
@@ -0,0 +1 @@
1
+ In forested areas.
@@ -0,0 +1 @@
1
+ In rocky areas. Leaves densely hairy.
@@ -0,0 +1 @@
1
+ Usually in moist or forested areas. [Phyllaries](./g/phyllary.html) with black hairs.
@@ -0,0 +1 @@
1
+ Flowers funnel shaped. Leaves spine-tipped.
@@ -0,0 +1 @@
1
+ In dry areas. Silvery leaves. Yellow patch on banner petals.
@@ -1 +1 @@
1
- Fruit densely hairy. Upper keel margin hairy from base to middle, not hairy from middle to tip
1
+ In moist areas or open forest. Fruit densely hairy. Upper keel margin hairy from base to middle, not hairy from middle to tip. Stem not hollow. Often more than 1 meter tall.
@@ -0,0 +1 @@
1
+ Leaves hairy. Plant mostly prostrate; inflorescence low.
@@ -0,0 +1 @@
1
+ In wet areas. Leaves and keel margins generally not hairy. Stem hollow.
@@ -0,0 +1 @@
1
+ Inflorescence hairy. Beak 3-5 mm (shorter than _P. groenlandica_).
@@ -0,0 +1 @@
1
+ Inflorescence not hairy. Beak 6-13 mm (longer than _P. attolens_).
@@ -0,0 +1 @@
1
+ Leaves generally wider (8-18 mm) than var. pedicellatus. Upper lip white.
@@ -0,0 +1 @@
1
+ Leaves generally narrower (<= 8 mm) than var. deustus. Upper lip brown.
@@ -0,0 +1 @@
1
+ Usually on dry mountain slopes. Inflorescence with glandular hairs. Similar to _P. rydbergii_.
@@ -0,0 +1 @@
1
+ Generally in high elevation wet habitats. Usually not hairy. Similar to _P. heterodoxus_.
@@ -0,0 +1 @@
1
+ Flowers not funnel shaped (base rounded). Leaves not spine-tipped.
@@ -0,0 +1 @@
1
+ Petals with small notch at tips.
@@ -0,0 +1 @@
1
+ Leaves palmate, with 5-7 toothed leaflets.
@@ -1 +1 @@
1
- Stem hairy and glandular.
1
+ Stem hairy and glandular. Intergrades with _P. tetrandra_.
@@ -1 +1 @@
1
- Stem not hairy.
1
+ Stem not hairy. Intergrades with _P. jeffreyi_.
@@ -0,0 +1 @@
1
+ Leaves gray. Scented.
@@ -0,0 +1 @@
1
+ Leaves green. Scented.
@@ -0,0 +1 @@
1
+ Leaves green. Scented.
@@ -0,0 +1 @@
1
+ Leaf blades round, with petioles.
@@ -0,0 +1 @@
1
+ Flower bract not longer than [pedicel](./g/pedicel.html).
@@ -0,0 +1 @@
1
+ Leaves with wide white veins. Flower bract not longer than [pedicel](./g/pedicel.html).
@@ -0,0 +1 @@
1
+ Leaves generally pointed at both ends. Older leaves usually hairless; younger leaves often somewhat hairy.
@@ -0,0 +1 @@
1
+ Leaves wider at end than at base; base generally wedge-shaped.
@@ -0,0 +1 @@
1
+ Usually around 13 ray flowers. Leaves not triangular, larger and more numerous towards base. Phyllary tips black.
@@ -0,0 +1 @@
1
+ Usually around 13 ray flowers. Leaves not triangular, larger and more numerous towards base. Phyllary tips green.
@@ -0,0 +1 @@
1
+ Usually 8 ray flowers. Leaves triangular and toothed; evenly distributed along stem.
@@ -0,0 +1 @@
1
+ Hairs reddish brown and sparse. Intergrades with [_S. scopulina_](./Sorbus-scopulina.html).
@@ -0,0 +1 @@
1
+ Hairs whitish.
@@ -1,40 +1,47 @@
1
1
  * {
2
- font-family: Georgia, 'Times New Roman', Times, serif;
2
+ font-family: Georgia, 'Times New Roman', Times, serif;
3
3
  }
4
4
 
5
5
  div.flr {
6
- background-color: lightgray;
7
- display: flex;
8
- align-items: center;
6
+ background-color: lightgray;
7
+ display: flex;
8
+ align-items: center;
9
+ }
10
+
11
+ div.hdr {
12
+ display: flex;
13
+ column-gap: 2rem;
14
+ row-gap: .5rem;
15
+ flex-flow: row wrap;
16
+ justify-content: space-between;
9
17
  }
10
18
 
11
19
  div.section {
12
- margin-bottom: .5rem;
20
+ margin-bottom: .5rem;
13
21
  }
14
22
 
15
23
  div.photos {
16
- display: flex;
24
+ display: flex;
17
25
  }
18
26
 
19
27
  figure {
20
- margin: auto;
28
+ margin: auto;
21
29
  }
22
30
 
23
31
  img.flr-color {
24
- padding-right: .25rem;
25
- width: 1rem;
32
+ padding-right: .25rem;
33
+ width: 1rem;
26
34
  }
27
35
 
28
36
  span.flr-time,
29
37
  span.lc {
30
- font-weight: bold;
38
+ font-weight: bold;
31
39
  }
32
40
 
33
41
  span.lcs {
34
- margin-right: 1rem;
42
+ margin-right: 1rem;
35
43
  }
36
44
 
37
- /* Glossary */
38
45
  div.glossary img {
39
- max-height: 300px;
46
+ width: 100%
40
47
  }
package/lib/csv.js CHANGED
@@ -134,7 +134,6 @@ export class CSV {
134
134
  }
135
135
 
136
136
  /**
137
- *
138
137
  * @param {string} fileName
139
138
  * @param {string[][]} data
140
139
  * @param {string[]} [headerData]
@@ -146,7 +145,6 @@ export class CSV {
146
145
  }
147
146
 
148
147
  /**
149
- *
150
148
  * @param {string} fileName
151
149
  * @param {Object<string,any>[]} data
152
150
  * @param {string[]} headerData
@@ -4,193 +4,193 @@ import { XHTML } from "./xhtml.js";
4
4
  import { Config } from "../config.js";
5
5
 
6
6
  const MEDIA_TYPES = {
7
- SVG: "image/svg+xml",
8
- JPG: "image/jpeg",
9
- XHTML: "application/xhtml+xml",
7
+ SVG: "image/svg+xml",
8
+ JPG: "image/jpeg",
9
+ XHTML: "application/xhtml+xml",
10
10
  };
11
11
 
12
12
  /**
13
13
  * @type {Object<string,string>}
14
14
  */
15
15
  const EXT_MEDIA_MAP = {
16
- jpeg: MEDIA_TYPES.JPG,
17
- jpg: MEDIA_TYPES.JPG,
18
- svg: MEDIA_TYPES.SVG,
16
+ jpeg: MEDIA_TYPES.JPG,
17
+ jpg: MEDIA_TYPES.JPG,
18
+ svg: MEDIA_TYPES.SVG,
19
19
  };
20
20
 
21
21
  class EBook {
22
- #outputDir;
23
- #filename;
24
- #pub_id;
25
- #title;
26
-
27
- /**
28
- * @param {string} outputDir
29
- * @param {string} filename
30
- * @param {string} pub_id
31
- * @param {string} title
32
- */
33
- constructor(outputDir, filename, pub_id, title) {
34
- this.#outputDir = outputDir;
35
- this.#filename = filename;
36
- this.#pub_id = pub_id;
37
- this.#title = title;
38
-
39
- // Initialize output directory.
40
- fs.rmSync(this.#outputDir, { force: true, recursive: true });
41
- fs.mkdirSync(this.getContentDir(), { recursive: true });
42
- }
43
-
44
- async create() {
45
- const contentDir = this.getContentDir();
46
-
47
- this.#createContainerFile();
48
- await this.createPages();
49
- this.#createPackageFile();
50
-
51
- // Copy assets
52
- fs.cpSync(Config.getPackageDir() + "/ebook", contentDir, {
53
- recursive: true,
54
- });
55
-
56
- this.createZip();
57
- }
58
-
59
- #createContainerFile() {
60
- const metaDir = this.#getMetaDir();
61
-
62
- fs.mkdirSync(metaDir, { recursive: true });
63
-
64
- let xml =
65
- '<?xml version="1.0"?>' +
66
- '<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">';
67
-
68
- xml +=
69
- '<rootfiles><rootfile full-path="epub/package.opf" media-type="application/oebps-package+xml" /></rootfiles>';
70
- xml += "</container>";
71
-
72
- fs.writeFileSync(metaDir + "/container.xml", xml);
73
- }
74
-
75
- #createPackageFile() {
76
- const dir = this.getContentDir();
77
-
78
- let xml =
79
- '<?xml version="1.0"?>\n' +
80
- '<package version="3.0" xml:lang="en" xmlns="http://www.idpf.org/2007/opf" unique-identifier="pub-id">';
81
-
82
- xml += this.#renderMetadata();
83
- xml += this.#renderManifest();
84
- xml += this.#renderSpine();
85
-
86
- xml += "</package>";
87
-
88
- fs.writeFileSync(dir + "/package.opf", xml);
89
- }
90
-
91
- async createPages() {
92
- throw new Error("must be implemented by subclass");
93
- }
94
-
95
- createZip() {
96
- // Create zip.
97
- const filename = this.#outputDir + "/" + this.#filename + ".epub";
98
- const output = fs.createWriteStream(filename);
99
- const archive = archiver("zip", {
100
- zlib: { level: 9 }, // Sets the compression level.
101
- });
102
-
103
- archive.pipe(output);
104
-
105
- archive.append("application/epub+zip", {
106
- name: "mimetype",
107
- store: true,
108
- });
109
-
110
- archive.directory(this.#getMetaDir(), "META-INF");
111
- archive.directory(this.getContentDir(), "epub");
112
-
113
- archive.finalize();
114
- }
115
-
116
- getContentDir() {
117
- return this.#outputDir + "/epub";
118
- }
119
-
120
- /**
121
- * @param {string} id
122
- * @param {string} href
123
- * @param {string} mediaType
124
- */
125
- static getManifestEntry(id, href, mediaType = MEDIA_TYPES.XHTML) {
126
- return (
127
- '<item id="' +
128
- id +
129
- '" href="' +
130
- href +
131
- '" media-type="' +
132
- mediaType +
133
- '" />'
134
- );
135
- }
136
-
137
- /**
138
- * @param {string} ext
139
- */
140
- static getMediaTypeForExt(ext) {
141
- return EXT_MEDIA_MAP[ext];
142
- }
143
-
144
- /**
145
- * @param {string} id
146
- */
147
- static getSpineEntry(id) {
148
- return '<itemref idref="' + id + '"/>';
149
- }
150
-
151
- #getMetaDir() {
152
- return this.#outputDir + "/META-INF";
153
- }
154
-
155
- #renderManifest() {
156
- let xml = "<manifest>";
157
- xml += '<item id="c0" href="css/main.css" media-type="text/css" />';
158
- xml += this.renderManifestEntries();
159
- xml +=
160
- '<item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />';
161
- return xml + "</manifest>";
162
- }
163
-
164
- renderManifestEntries() {
165
- throw new Error("must be implemented by subclass");
166
- }
167
-
168
- #renderMetadata() {
169
- let xml = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">';
170
- xml += XHTML.textElement("dc:identifier", this.#pub_id, {
171
- id: "pub-id",
172
- });
173
- xml += "<dc:language>en-US</dc:language>";
174
- xml += XHTML.textElement("dc:title", this.#title);
175
- const d = new Date();
176
- d.setUTCMilliseconds(0);
177
- xml +=
178
- '<meta property="dcterms:modified">' +
179
- d.toISOString().replace(".000", "") +
180
- "</meta>";
181
- return xml + "</metadata>";
182
- }
183
-
184
- #renderSpine() {
185
- let xml = "<spine>";
186
- xml += '<itemref idref="toc"/>';
187
- xml += this.renderSpineElements();
188
- return xml + "</spine>";
189
- }
190
-
191
- renderSpineElements() {
192
- throw new Error("must be implemented by subclass");
193
- }
22
+ #outputDir;
23
+ #filename;
24
+ #pub_id;
25
+ #title;
26
+
27
+ /**
28
+ * @param {string} outputDir
29
+ * @param {string} filename
30
+ * @param {string} pub_id
31
+ * @param {string} title
32
+ */
33
+ constructor(outputDir, filename, pub_id, title) {
34
+ this.#outputDir = outputDir;
35
+ this.#filename = filename;
36
+ this.#pub_id = pub_id;
37
+ this.#title = title;
38
+
39
+ // Initialize output directory.
40
+ fs.rmSync(this.#outputDir, { force: true, recursive: true });
41
+ fs.mkdirSync(this.getContentDir(), { recursive: true });
42
+ }
43
+
44
+ async create() {
45
+ const contentDir = this.getContentDir();
46
+
47
+ this.#createContainerFile();
48
+ await this.createPages();
49
+ this.#createPackageFile();
50
+
51
+ // Copy assets
52
+ fs.cpSync(Config.getPackageDir() + "/ebook", contentDir, {
53
+ recursive: true,
54
+ });
55
+
56
+ this.createZip();
57
+ }
58
+
59
+ #createContainerFile() {
60
+ const metaDir = this.#getMetaDir();
61
+
62
+ fs.mkdirSync(metaDir, { recursive: true });
63
+
64
+ let xml =
65
+ '<?xml version="1.0"?>' +
66
+ '<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">';
67
+
68
+ xml +=
69
+ '<rootfiles><rootfile full-path="epub/package.opf" media-type="application/oebps-package+xml" /></rootfiles>';
70
+ xml += "</container>";
71
+
72
+ fs.writeFileSync(metaDir + "/container.xml", xml);
73
+ }
74
+
75
+ #createPackageFile() {
76
+ const dir = this.getContentDir();
77
+
78
+ let xml =
79
+ '<?xml version="1.0"?>\n' +
80
+ '<package version="3.0" xml:lang="en" xmlns="http://www.idpf.org/2007/opf" unique-identifier="pub-id">';
81
+
82
+ xml += this.#renderMetadata();
83
+ xml += this.#renderManifest();
84
+ xml += this.#renderSpine();
85
+
86
+ xml += "</package>";
87
+
88
+ fs.writeFileSync(dir + "/package.opf", xml);
89
+ }
90
+
91
+ async createPages() {
92
+ throw new Error("must be implemented by subclass");
93
+ }
94
+
95
+ createZip() {
96
+ // Create zip.
97
+ const filename = this.#outputDir + "/" + this.#filename + ".epub";
98
+ const output = fs.createWriteStream(filename);
99
+ const archive = archiver("zip", {
100
+ zlib: { level: 9 }, // Sets the compression level.
101
+ });
102
+
103
+ archive.pipe(output);
104
+
105
+ archive.append("application/epub+zip", {
106
+ name: "mimetype",
107
+ store: true,
108
+ });
109
+
110
+ archive.directory(this.#getMetaDir(), "META-INF");
111
+ archive.directory(this.getContentDir(), "epub");
112
+
113
+ archive.finalize();
114
+ }
115
+
116
+ getContentDir() {
117
+ return this.#outputDir + "/epub";
118
+ }
119
+
120
+ /**
121
+ * @param {string} id
122
+ * @param {string} href
123
+ * @param {string} mediaType
124
+ */
125
+ static getManifestEntry(id, href, mediaType = MEDIA_TYPES.XHTML) {
126
+ return (
127
+ '<item id="' +
128
+ id +
129
+ '" href="' +
130
+ href +
131
+ '" media-type="' +
132
+ mediaType +
133
+ '" />'
134
+ );
135
+ }
136
+
137
+ /**
138
+ * @param {string} ext
139
+ */
140
+ static getMediaTypeForExt(ext) {
141
+ return EXT_MEDIA_MAP[ext];
142
+ }
143
+
144
+ /**
145
+ * @param {string} id
146
+ */
147
+ static getSpineEntry(id) {
148
+ return '<itemref idref="' + id + '"/>';
149
+ }
150
+
151
+ #getMetaDir() {
152
+ return this.#outputDir + "/META-INF";
153
+ }
154
+
155
+ #renderManifest() {
156
+ let xml = "<manifest>";
157
+ xml += '<item id="c0" href="css/main.css" media-type="text/css" />';
158
+ xml += this.renderManifestEntries();
159
+ xml +=
160
+ '<item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav" />';
161
+ return xml + "</manifest>";
162
+ }
163
+
164
+ renderManifestEntries() {
165
+ throw new Error("must be implemented by subclass");
166
+ }
167
+
168
+ #renderMetadata() {
169
+ let xml = '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">';
170
+ xml += XHTML.textElement("dc:identifier", this.#pub_id, {
171
+ id: "pub-id",
172
+ });
173
+ xml += "<dc:language>en-US</dc:language>";
174
+ xml += XHTML.textElement("dc:title", this.#title);
175
+ const d = new Date();
176
+ d.setUTCMilliseconds(0);
177
+ xml +=
178
+ '<meta property="dcterms:modified">' +
179
+ d.toISOString().replace(".000", "") +
180
+ "</meta>";
181
+ return xml + "</metadata>";
182
+ }
183
+
184
+ #renderSpine() {
185
+ let xml = "<spine>";
186
+ xml += '<itemref idref="toc"/>';
187
+ xml += this.renderSpineElements();
188
+ return xml + "</spine>";
189
+ }
190
+
191
+ renderSpineElements() {
192
+ throw new Error("must be implemented by subclass");
193
+ }
194
194
  }
195
195
 
196
196
  export { EBook };