@ca-plant-list/ca-plant-list 0.4.6 → 0.4.9

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,20 @@
1
+ on:
2
+ workflow_dispatch:
3
+ pull_request:
4
+ push:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ check:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: latest
17
+ cache: "npm"
18
+ - run: npm update
19
+ - run: npx eslint
20
+ - run: npx tsc
package/.prettierrc CHANGED
@@ -1 +1,3 @@
1
- {}
1
+ {
2
+ "tabWidth": 4
3
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "eslint.validate": ["javascript", "typescript"],
3
+ "json.schemas": [
4
+ {
5
+ "fileMatch": ["exceptions.json"],
6
+ "url": "./schemas/exceptions.schema.json"
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1 @@
1
+ The collection of petals and [sepals](./sepal.html) on a flower.
package/data/synonyms.csv CHANGED
@@ -59,6 +59,7 @@ Agrostis viridis,Polypogon viridis
59
59
  Aira caryophyllea var. capillaris,Aira elegans
60
60
  Aira elegantissima,Aira elegans
61
61
  Aira pulchella,Aira elegans
62
+ Alchemilla occidentalis,Aphanes occidentalis,INAT
62
63
  Allium acuminatum var. gracile,Allium amplectens
63
64
  Allium attenuifolium var. monospermum,Allium amplectens
64
65
  Allium attenuifolium,Allium amplectens
@@ -870,9 +871,9 @@ Helianthemum scoparium var. vulgare,Crocanthemum scoparium var. vulgare
870
871
  Helianthus annuus subsp. jaegeri,Helianthus annuus
871
872
  Helianthus annuus subsp. lenticularis,Helianthus annuus
872
873
  Helianthus annuus var. macrocarpus,Helianthus annuus
873
- Helosciadium nodiflorum,Apium nodiflorum,INAT
874
874
  Heliotropium oculatum,Heliotropium curassavicum var. oculatum
875
875
  Heliotropium spathulatum subsp. oculatum,Heliotropium curassavicum var. oculatum
876
+ Helosciadium nodiflorum,Apium nodiflorum,INAT
876
877
  Helxine soleirolii,Soleirolia soleirolii
877
878
  Hemizonia angustifolia,Deinandra corymbosa
878
879
  Hemizonia corymbosa subsp. macrocephala,Deinandra corymbosa
@@ -1779,6 +1780,7 @@ Ruppia occidentalis,Ruppia cirrhosa
1779
1780
  Ruppia spiralis,Ruppia cirrhosa
1780
1781
  Sagina apetala var. barbata,Sagina apetala
1781
1782
  Sagina occidentalis,Sagina decumbens subsp. occidentalis
1783
+ Sagittaria calycina,Sagittaria montevidensis subsp. calycina,INAT
1782
1784
  Sairocarpus multiflorus,Antirrhinum thompsonii,INAT
1783
1785
  Sairocarpus vexillocalyculatus subsp. vexillocalyculatus,Antirrhinum vexillocalyculatum subsp. vexillocalyculatum,INAT
1784
1786
  Salicornia subterminalis,Arthrocnemum subterminale
package/data/taxa.csv CHANGED
@@ -114,7 +114,7 @@ Antirrhinum kelloggii,,N,13568,401,165675
114
114
  Antirrhinum majus,snapdragon,X,13557,404,48969
115
115
  Antirrhinum thompsonii,Sierra snapdragon,N,108978,14286,168306,,pink,4,8
116
116
  Antirrhinum vexillocalyculatum subsp. vexillocalyculatum,,N,88873,10499,840919,annual,purple,6,8
117
- Aphanes occidentalis,western lady's mantle,N,13608,,1452907,annual,,3,5
117
+ Aphanes occidentalis,western lady's mantle,N,13608,420,1452907,annual,,3,5
118
118
  Aphyllon californicum subsp. jepsonii,,N,103325,13438,802459
119
119
  Aphyllon epigalium subsp. epigalium,,N,103327,13527,809377
120
120
  Aphyllon fasciculatum,clustered broom-rape,N,100018,13441,802543
@@ -1255,6 +1255,8 @@ Navarretia pubescens,downy pincushion,N,34471,5804,58976
1255
1255
  Navarretia squarrosa,skunkweed,N,34475,5807,53157
1256
1256
  Navarretia tagetina,,N,34477,5809,60226
1257
1257
  Navarretia viscidula,sticky navarretia,N,34478,5810,78195
1258
+ Nemacladus capillaris,common threadplant,N,34503,5815,78199,annual,white,5,7
1259
+ Nemacladus montanus,mountain threadplant,N,34508,5824,78203,annual,white,5,7
1258
1260
  Nemophila heterophylla,,N,34539,5834,53158,,white,2,6
1259
1261
  Nemophila menziesii var. atomaria,,N,62279,5837,50647,,white,2,6
1260
1262
  Nemophila menziesii var. menziesii,baby blue-eyes,N,62285,5839,50650
@@ -1582,6 +1584,7 @@ Sagina apetala,dwarf pearlwort,X,42572,7243,56998
1582
1584
  Sagina decumbens subsp. occidentalis,western pearlwort,N,52717,7244,57000
1583
1585
  Sagina procumbens,matted pearlwort,X,42584,7246,55667
1584
1586
  Sagittaria latifolia,,N,42617,7249,48070
1587
+ Sagittaria montevidensis subsp. calycina,hooded arrowhead,N,52726,7251,1506723,annual,white,7,8
1585
1588
  Salicornia depressa,glasswort,N,42663,10333,78930
1586
1589
  Salicornia pacifica,pickleweed,N,42666,12001,78931
1587
1590
  Salicornia rubra,,N,42661,7256,78933
@@ -1770,9 +1773,9 @@ Torilis arvensis,tall sock-destroyer,X,46743,8004,56846
1770
1773
  Torilis nodosa,short sock-destroyer,X,46747,8007,53304
1771
1774
  Torreyochloa pallida var. pauciflora,,N,67197,8010,81431
1772
1775
  Toxicodendron diversilobum,poison-oak,N,46791,8015,51080,,white
1773
- Toxicoscordion fremontii,zigadene,N,89163,11103,49649,,white
1774
- Toxicoscordion paniculatum,,N,46802,11105,79380
1775
- Toxicoscordion venenosum var. venenosum,death-camas,N,93851,11106,81432
1776
+ Toxicoscordion fremontii,zigadene,N,89163,11103,49649,bulb,white,2,6
1777
+ Toxicoscordion paniculatum,,N,46802,11105,79380,bulb,white,5,6
1778
+ Toxicoscordion venenosum var. venenosum,death-camas,N,93851,11106,81432,bulb,white,5,7
1776
1779
  Tradescantia fluminensis,,X,77137,8017,79382
1777
1780
  Tragopogon porrifolius,salsify,X,5449,8020,54141
1778
1781
  Trianthema portulacastrum,,N,46920,8023,79390
@@ -0,0 +1 @@
1
+ Leaves 8-30 mm wide. [Perianth](./perianth.html) parts 5-15 mm long.
@@ -0,0 +1 @@
1
+ Leaves 6-16 mm wide. [Perianth](./perianth.html) parts 2-6 mm long. Perianth parts unequal, outer parts generally without claws, inner parts with claws. Stamens at least as long as perianth parts.
@@ -0,0 +1 @@
1
+ Leaves 4-10 mm wide. [Perianth](./perianth.html) parts 4-6 mm long. Stamens at least as long as perianth parts.
@@ -12,6 +12,14 @@ div.section {
12
12
  margin-bottom: .5rem;
13
13
  }
14
14
 
15
+ div.photos {
16
+ display: flex;
17
+ }
18
+
19
+ figure {
20
+ margin: auto;
21
+ }
22
+
15
23
  img.flr-color {
16
24
  padding-right: .25rem;
17
25
  width: 1rem;
@@ -29,4 +37,4 @@ span.lcs {
29
37
  /* Glossary */
30
38
  div.glossary img {
31
39
  max-height: 300px;
32
- }
40
+ }
package/eslint.config.mjs CHANGED
@@ -1,13 +1,13 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+
4
+ /** @type {import('eslint').Linter.Config[]} */
1
5
  export default [
6
+ { files: ["**/*.{js,mjs,cjs}"], ignores: ["output/**"] },
2
7
  {
3
- rules: {
4
- indent: ["error", 4, {
5
- SwitchCase: 1,
6
- }],
7
-
8
- "linebreak-style": ["error", "unix"],
9
- quotes: ["error", "double"],
10
- semi: ["error", "always"],
8
+ languageOptions: {
9
+ globals: { ...globals.browser, ...globals.node, bootstrap: false },
11
10
  },
12
- }
11
+ },
12
+ pluginJs.configs.recommended,
13
13
  ];
@@ -1,20 +1,15 @@
1
1
  import * as fs from "node:fs";
2
- import path from "node:path";
3
2
 
4
3
  import sharp from "sharp";
5
4
 
6
5
  import { EBook } from "./ebook.js";
7
- import { Config } from "../config.js";
8
- import { CSV } from "../csv.js";
9
6
  import { Files } from "../files.js";
10
- import { TaxonImage } from "./taxonimage.js";
7
+ import { ProgressMeter } from "../progressmeter.js";
11
8
 
12
9
  class Images {
13
10
  #siteGenerator;
14
11
  #contentDir;
15
12
  #taxa;
16
- /** @type {Object<string,TaxonImage[]>} */
17
- #images = {};
18
13
 
19
14
  /**
20
15
  * @param {SiteGenerator} siteGenerator
@@ -27,53 +22,63 @@ class Images {
27
22
  this.#taxa = taxa;
28
23
  }
29
24
 
30
- async createImages() {
25
+ /**
26
+ * @param {Taxon[]} taxa
27
+ */
28
+ async createImages(taxa) {
29
+ const meter = new ProgressMeter("processing photos", taxa.length);
30
+
31
+ const width = 300;
32
+ const quality = 40;
33
+
31
34
  const photoDirSrc = "external_data/photos";
32
- const imagePrefix = "i";
33
- const photoDirTarget = this.#contentDir + "/" + imagePrefix;
34
- fs.mkdirSync(photoDirSrc, { recursive: true });
35
- fs.mkdirSync(photoDirTarget, { recursive: true });
36
-
37
- const rows = CSV.parseFile(
38
- Config.getPackageDir() + "/data",
39
- "photos.csv"
35
+ const photoDirCache = `external_data/photos-${width}-${quality}`;
36
+ [photoDirSrc, photoDirCache, this.#contentDir + "/i"].forEach((dir) =>
37
+ fs.mkdirSync(dir, { recursive: true }),
40
38
  );
41
- for (const row of rows) {
42
- const name = row["taxon_name"];
43
- const taxon = this.#taxa.getTaxon(name);
44
- if (!taxon) {
45
- continue;
46
- }
47
39
 
48
- let imageList = this.#images[name];
49
- if (!imageList) {
50
- imageList = [];
51
- this.#images[name] = imageList;
40
+ let downloadCount = 0;
41
+ let compressCount = 0;
42
+ let copyCount = 0;
43
+
44
+ for (let index = 0; index < taxa.length; index++) {
45
+ const taxon = taxa[index];
46
+ const photos = Images.getTaxonPhotos(taxon);
47
+ for (const photo of photos) {
48
+ const ext = photo.getExt();
49
+ const cachedFileName = `${photoDirCache}/${this.getCompressedImageName(photo)}`;
50
+
51
+ if (!fs.existsSync(cachedFileName)) {
52
+ const srcFileName = `${photoDirSrc}/${photo.getId()}.${ext}`;
53
+ // Compress and cache source file.
54
+ if (!fs.existsSync(srcFileName)) {
55
+ // Retrieve original file.
56
+ if (!fs.existsSync(srcFileName)) {
57
+ // File is not there; retrieve it.
58
+ await Files.fetch(photo.getUrl(), srcFileName);
59
+ downloadCount++;
60
+ }
61
+ }
62
+ await sharp(srcFileName)
63
+ .resize({ width: width })
64
+ .jpeg({ quality: quality })
65
+ .toFile(cachedFileName);
66
+ compressCount++;
67
+ }
68
+
69
+ fs.copyFileSync(
70
+ cachedFileName,
71
+ this.getCompressedFilePath(photo),
72
+ );
73
+ copyCount++;
52
74
  }
53
-
54
- const src = new URL(row["source"]);
55
- const parts = path.parse(src.pathname).dir.split("/");
56
- const prefix = src.host.includes("calflora") ? "cf-" : "inat-";
57
- const filename = prefix + parts.slice(-1)[0] + ".jpg";
58
- const srcFileName = photoDirSrc + "/" + filename;
59
- const targetFileName = photoDirTarget + "/" + filename;
60
-
61
- if (!fs.existsSync(srcFileName)) {
62
- // File is not there; retrieve it.
63
- console.log("retrieving " + srcFileName);
64
- await Files.fetch(src, srcFileName);
65
- }
66
-
67
- await sharp(srcFileName)
68
- .resize({ width: 300 })
69
- .jpeg({ quality: 40 })
70
- .toFile(targetFileName);
71
-
72
- imageList.push(
73
- new TaxonImage(imagePrefix + "/" + filename, row["credit"])
74
- );
75
+ meter.update(index + 1, {
76
+ custom: ` | ${copyCount} copied - ${compressCount} compressed - ${downloadCount} downloaded`,
77
+ });
75
78
  }
76
79
 
80
+ meter.stop();
81
+
77
82
  this.#siteGenerator.copyIllustrations(this.#taxa.getFlowerColors());
78
83
  }
79
84
 
@@ -88,8 +93,8 @@ class Images {
88
93
  EBook.getManifestEntry(
89
94
  "i" + index,
90
95
  "i/" + fileName,
91
- EBook.getMediaTypeForExt(ext)
92
- )
96
+ EBook.getMediaTypeForExt(ext),
97
+ ),
93
98
  );
94
99
  }
95
100
 
@@ -97,10 +102,32 @@ class Images {
97
102
  }
98
103
 
99
104
  /**
100
- * @param {string} name
105
+ * @param {Photo} photo
106
+ * @returns {string}
107
+ */
108
+ getCompressedFilePath(photo) {
109
+ return `${this.#contentDir}/i/${this.getCompressedImageName(photo)}`;
110
+ }
111
+
112
+ /**
113
+ * @param {Photo} photo
114
+ * @returns {string}
101
115
  */
102
- getTaxonImages(name) {
103
- return this.#images[name];
116
+ getCompressedImageName(photo) {
117
+ return `${photo.getId()}.${photo.getExt().toLowerCase()}`;
118
+ }
119
+
120
+ /**
121
+ * @param {Taxon} taxon
122
+ * @returns {Photo[]}
123
+ */
124
+ static getTaxonPhotos(taxon) {
125
+ const photos = taxon
126
+ .getPhotos()
127
+ .filter((photo) =>
128
+ ["jpg", "jpeg"].includes(photo.getExt().toLowerCase()),
129
+ );
130
+ return photos.length > 0 ? [photos[0]] : photos;
104
131
  }
105
132
  }
106
133
 
@@ -1,26 +1,25 @@
1
- import imageSize from "image-size";
2
1
  import { EBookPage } from "../ebookpage.js";
3
2
  import { XHTML } from "../xhtml.js";
4
3
  import { Markdown } from "../../markdown.js";
5
4
  import { HTMLTaxon } from "../../htmltaxon.js";
6
5
  import { Config } from "../../config.js";
7
6
  import { Files } from "../../files.js";
7
+ import { Images } from "../images.js";
8
+ import imageSize from "image-size";
8
9
 
9
10
  class TaxonPage extends EBookPage {
10
- #outputDir;
11
11
  #taxon;
12
- #photos;
12
+ #images;
13
13
 
14
14
  /**
15
15
  * @param {string} outputDir
16
16
  * @param {Taxon} taxon
17
- * @param {TaxonImage[]} photos
17
+ * @param {Images} images
18
18
  */
19
- constructor(outputDir, taxon, photos) {
19
+ constructor(outputDir, taxon, images) {
20
20
  super(outputDir + "/" + taxon.getFileName(), taxon.getName());
21
- this.#outputDir = outputDir;
22
21
  this.#taxon = taxon;
23
- this.#photos = photos;
22
+ this.#images = images;
24
23
  }
25
24
 
26
25
  renderPageBody() {
@@ -45,7 +44,7 @@ class TaxonPage extends EBookPage {
45
44
  html += XHTML.wrap(
46
45
  "div",
47
46
  XHTML.getLink(family.getFileName(), family.getName()),
48
- { class: "section" }
47
+ { class: "section" },
49
48
  );
50
49
 
51
50
  const cn = this.#taxon.getCommonNames();
@@ -59,26 +58,25 @@ class TaxonPage extends EBookPage {
59
58
 
60
59
  html += renderCustomText(this.#taxon.getBaseFileName());
61
60
 
62
- if (this.#photos) {
63
- let photoHTML = "";
64
- for (const photo of this.#photos) {
65
- const src = photo.getSrc();
66
- const dimensions = imageSize.imageSize(
67
- this.#outputDir + "/" + src
68
- );
69
- let img = XHTML.textElement("img", "", {
70
- src: src,
71
- style: "max-width:" + dimensions.width + "px",
72
- });
73
- const caption = photo.getCaption();
74
- if (caption) {
75
- img += XHTML.textElement("figcaption", caption);
76
- }
77
- photoHTML += XHTML.wrap("figure", img);
78
- }
79
- if (photoHTML) {
80
- html += XHTML.wrap("div", photoHTML);
61
+ const photos = Images.getTaxonPhotos(this.#taxon);
62
+
63
+ let photoHTML = "";
64
+ for (const photo of photos) {
65
+ const dimensions = imageSize.imageSize(
66
+ this.#images.getCompressedFilePath(photo),
67
+ );
68
+ let img = XHTML.textElement("img", "", {
69
+ src: `i/${this.#images.getCompressedImageName(photo)}`,
70
+ style: "max-width:" + dimensions.width + "px",
71
+ });
72
+ const caption = `${photo.rights === "CC0" ? "By" : "(c)"} ${photo.rightsHolder} ${photo.rights && `(${photo.rights})`}`;
73
+ if (caption) {
74
+ img += XHTML.textElement("figcaption", caption);
81
75
  }
76
+ photoHTML += XHTML.wrap("figure", img);
77
+ }
78
+ if (photoHTML) {
79
+ html += XHTML.wrap("div", photoHTML, "photos");
82
80
  }
83
81
 
84
82
  return html;