@ca-plant-list/ca-plant-list 0.2.0 → 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.
@@ -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
+ }
@@ -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 };
@@ -0,0 +1,7 @@
1
+ import { HTML } from "@ca-plant-list/ca-plant-list";
2
+
3
+ class XHTML extends HTML {
4
+
5
+ }
6
+
7
+ export { XHTML };
package/lib/index.d.ts CHANGED
@@ -135,10 +135,11 @@ import { Families } from "./families.js";
135
135
  import { Files } from "./files.js";
136
136
  import { HTML } from "./html.js";
137
137
  import { Jekyll } from "./jekyll.js";
138
+ import { PlantBook } from "./ebook/plantbook.js";
138
139
  import { Taxa } from "./taxa.js";
139
140
  import { TAXA_COLNAMES } from "./taxon.js";
140
141
  import { Taxon } from "./taxon.js";
141
- export { BasePageRenderer, Config, CSV, DataLoader, ErrorLog, Exceptions, Families, Files, HTML, Jekyll, Taxa, TAXA_COLNAMES, Taxon };
142
+ export { BasePageRenderer, Config, CSV, DataLoader, ErrorLog, Exceptions, Families, Files, HTML, Jekyll, PlantBook, Taxa, TAXA_COLNAMES, Taxon };
142
143
  export class Jekyll {
143
144
  static hasInclude(baseDir: any, path: any): boolean;
144
145
  static include(path: any): string;
@@ -205,6 +206,7 @@ export namespace TAXA_LIST_COLS {
205
206
  import { Taxon } from "./taxon.js";
206
207
  export namespace TAXA_COLNAMES {
207
208
  const COMMON_NAME: string;
209
+ const FLOWER_COLOR: string;
208
210
  }
209
211
  export class Taxon {
210
212
  constructor(data: any);
@@ -219,6 +221,7 @@ export class Taxon {
219
221
  getFamily(): any;
220
222
  getFESA(): any;
221
223
  getFileName(ext?: string): string;
224
+ getFlowerColors(): any;
222
225
  getGenus(): {
223
226
  "__#5@#data": any;
224
227
  getTaxa(): any;
package/lib/index.js CHANGED
@@ -8,7 +8,8 @@ import { Families } from "./families.js";
8
8
  import { Files } from "./files.js";
9
9
  import { HTML } from "./html.js";
10
10
  import { Jekyll } from "./jekyll.js";
11
+ import { PlantBook } from "./ebook/plantbook.js";
11
12
  import { Taxa } from "./taxa.js";
12
13
  import { Taxon, TAXA_COLNAMES } from "./taxon.js";
13
14
 
14
- export { BasePageRenderer, Config, CSV, DataLoader, ErrorLog, Exceptions, Families, Files, HTML, Jekyll, Taxa, TAXA_COLNAMES, Taxon };
15
+ export { BasePageRenderer, Config, CSV, DataLoader, ErrorLog, Exceptions, Families, Files, HTML, Jekyll, PlantBook, Taxa, TAXA_COLNAMES, Taxon };
package/lib/taxon.js CHANGED
@@ -4,7 +4,8 @@ import { HTML } from "./html.js";
4
4
  import { RarePlants } from "./rareplants.js";
5
5
 
6
6
  const TAXA_COLNAMES = {
7
- COMMON_NAME: "common name"
7
+ COMMON_NAME: "common name",
8
+ FLOWER_COLOR: "flower_color",
8
9
  };
9
10
 
10
11
  class Taxon {
@@ -18,6 +19,7 @@ class Taxon {
18
19
  #cfSyn;
19
20
  #iNatID;
20
21
  #iNatSyn;
22
+ #flowerColors;
21
23
  #rpiID;
22
24
  #rankRPI;
23
25
  #cesa;
@@ -40,6 +42,8 @@ class Taxon {
40
42
  this.#jepsonID = data[ "jepson id" ];
41
43
  this.#calRecNum = data[ "calrecnum" ];
42
44
  this.#iNatID = data[ "inat id" ];
45
+ const colors = data[ TAXA_COLNAMES.FLOWER_COLOR ];
46
+ this.#flowerColors = colors ? colors.split( "," ) : undefined;
43
47
  this.#rpiID = data[ "RPI ID" ];
44
48
  this.#rankRPI = data[ "CRPR" ];
45
49
  this.#cesa = cesa ? cesa : undefined;
@@ -112,6 +116,10 @@ class Taxon {
112
116
  return this.getBaseFileName() + "." + ext;
113
117
  }
114
118
 
119
+ getFlowerColors() {
120
+ return this.#flowerColors;
121
+ }
122
+
115
123
  getGenus() {
116
124
  return Genera.getGenus( this.#genus );
117
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Tools to create Jekyll files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,13 +18,18 @@
18
18
  },
19
19
  "types": "./lib/index.d.ts",
20
20
  "bin": {
21
- "ca-plant-list": "scripts/build-site.js"
21
+ "ca-plant-list": "scripts/build-site.js",
22
+ "ca-plant-book": "scripts/build-ebook.js"
22
23
  },
23
24
  "dependencies": {
24
25
  "@ca-plant-list/tools": "file:../ca-tools",
26
+ "archiver": "^5.3.1",
25
27
  "command-line-args": "^5.2.1",
26
28
  "command-line-usage": "^6.1.3",
27
29
  "csv-parse": "^5.3.1",
30
+ "image-size": "^1.0.2",
31
+ "markdown-it": "^13.0.1",
32
+ "sharp": "^0.32.1",
28
33
  "unzipper": "^0.10.11"
29
34
  },
30
35
  "devDependencies": {
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import commandLineArgs from "command-line-args";
4
+ import { PlantBook } from "@ca-plant-list/ca-plant-list";
5
+ import { DataLoader } from "../lib/dataloader.js";
6
+
7
+ const options = commandLineArgs( DataLoader.getOptionDefs() );
8
+
9
+ DataLoader.load( options );
10
+
11
+ const ebook = new PlantBook();
12
+ await ebook.create();