@ca-plant-list/ca-plant-list 0.0.2 → 0.1.0

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/build-site.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import * as path from "node:path";
4
- import * as url from "node:url";
5
3
  import { Config } from "./lib/config.js";
6
4
  import { DataLoader } from "./lib/dataloader.js";
7
5
  import { ErrorLog } from "./lib/errorlog.js";
@@ -16,12 +14,10 @@ const options = commandLineArgs( OPTION_DEFS );
16
14
 
17
15
  const TAXA_DATA_DIR = options.data;
18
16
  const CONFIG_DATA_DIR = "./data";
19
- const PACKAGE_DIR = path.dirname( url.fileURLToPath( import.meta.url ) );
20
- const DEFAULT_DATA_DIR = PACKAGE_DIR + "/data";
21
17
  const OUTPUT_DIR = "./output";
22
18
 
23
19
  Config.init( CONFIG_DATA_DIR );
24
- DataLoader.load( TAXA_DATA_DIR, DEFAULT_DATA_DIR );
25
- PageRenderer.render( PACKAGE_DIR, OUTPUT_DIR );
20
+ DataLoader.load( TAXA_DATA_DIR );
21
+ PageRenderer.render( OUTPUT_DIR );
26
22
 
27
23
  ErrorLog.write( OUTPUT_DIR + "/errors.txt" );
@@ -17,6 +17,15 @@
17
17
  background-color: #3D550C;
18
18
  }
19
19
 
20
+ span[title] {
21
+ text-decoration: underline dotted;
22
+ }
23
+
24
+ span.label {
25
+ font-weight: 600;
26
+ padding-right: .5rem;
27
+ }
28
+
20
29
  td {
21
30
  border: solid 1px;
22
31
  vertical-align: top;
@@ -28,9 +37,16 @@ th {
28
37
 
29
38
  /* Lists */
30
39
 
31
- a.native {
40
+ span.native {
32
41
  font-weight: bold;
33
- font-style: italic;
42
+ }
43
+
44
+ span.rare {
45
+ font-weight: bold;
46
+ }
47
+
48
+ span.rare::before {
49
+ content: "\2606";
34
50
  }
35
51
 
36
52
  /* Taxon Page */
@@ -62,4 +78,8 @@ div.section ul {
62
78
 
63
79
  div.section li {
64
80
  display: block;
81
+ }
82
+
83
+ div.section ul.indent {
84
+ padding-left: 1rem;
65
85
  }
package/lib/config.js CHANGED
@@ -1,8 +1,11 @@
1
+ import * as path from "node:path";
2
+ import * as url from "node:url";
1
3
  import * as fs from "node:fs";
2
4
 
3
5
  class Config {
4
6
 
5
7
  static #config = {};
8
+ static #packageDir = path.dirname( path.dirname( url.fileURLToPath( import.meta.url ) ) );
6
9
 
7
10
  static getConfigValue( prefix, name, dflt ) {
8
11
  const obj = this.#config[ prefix ];
@@ -18,6 +21,10 @@ class Config {
18
21
  return this.getConfigValue( "labels", name, dflt );
19
22
  }
20
23
 
24
+ static getPackageDir() {
25
+ return this.#packageDir;
26
+ }
27
+
21
28
  static init( dir ) {
22
29
  try {
23
30
  this.#config = JSON.parse( fs.readFileSync( dir + "/config.json" ) );
package/lib/dataloader.js CHANGED
@@ -1,15 +1,21 @@
1
1
  import { Taxa } from "./taxa.js";
2
2
  import { Genera } from "./genera.js";
3
3
  import { Families } from "./families.js";
4
+ import { Config } from "./config.js";
5
+ import { Exceptions } from "./exceptions.js";
6
+
4
7
 
5
8
  class DataLoader {
6
9
 
7
- static load( taxaDir, dataDir ) {
10
+ static load( taxaDir ) {
11
+
12
+ const defaultDataDir = Config.getPackageDir() + "/data";
8
13
 
9
14
  console.log( "loading data" );
10
15
 
11
- Families.init( dataDir );
12
- Genera.init( dataDir );
16
+ Exceptions.init( taxaDir );
17
+ Families.init( defaultDataDir );
18
+ Genera.init( defaultDataDir );
13
19
  Taxa.init( taxaDir );
14
20
 
15
21
  }
@@ -0,0 +1,28 @@
1
+ import * as fs from "node:fs";
2
+
3
+ class Exceptions {
4
+
5
+ static #exceptions = {};
6
+
7
+ static hasException( name, cat, subcat ) {
8
+ const taxonData = this.#exceptions[ name ];
9
+ if ( taxonData ) {
10
+ const catData = taxonData[ cat ];
11
+ if ( catData ) {
12
+ return catData[ subcat ] !== undefined;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+
18
+ static init( dir ) {
19
+ try {
20
+ this.#exceptions = JSON.parse( fs.readFileSync( dir + "/exceptions.json" ) );
21
+ } catch ( e ) {
22
+ console.log( e );
23
+ }
24
+ }
25
+
26
+ }
27
+
28
+ export { Exceptions };
package/lib/html.js CHANGED
@@ -29,7 +29,10 @@ class HTML {
29
29
  }
30
30
 
31
31
  static getLink( href, linkText, attributes = {}, options = 0 ) {
32
- let html = "<a href=\"" + this.escapeAttribute( href ) + "\"";
32
+ let html = "<a";
33
+ if ( href !== undefined ) {
34
+ html += this.renderAttribute( "href", href );
35
+ }
33
36
  html += this.renderAttributes( attributes );
34
37
  if ( options & HTML_OPTIONS.OPEN_NEW ) {
35
38
  html += this.renderAttribute( "target", "_blank" );
@@ -1,19 +1,20 @@
1
1
  import * as fs from "node:fs";
2
2
  import { Families } from "./families.js";
3
- import { HTML } from "./html.js";
3
+ import { HTML, HTML_OPTIONS } from "./html.js";
4
4
  import { Taxa } from "./taxa.js";
5
5
  import { HTMLPage } from "./htmlpage.js";
6
6
  import { PageTaxon } from "./pagetaxon.js";
7
7
  import { Config } from "./config.js";
8
+ import { RPI } from "./rpi.js";
8
9
 
9
10
  class PageRenderer {
10
11
 
11
- static render( packageDir, outputDir ) {
12
+ static render( outputDir ) {
12
13
 
13
14
  // Copy static files
14
15
  fs.rmSync( outputDir, { force: true, recursive: true } );
15
16
  // First copy default Jekyll files from package.
16
- fs.cpSync( packageDir + "/jekyll", outputDir, { recursive: true } );
17
+ fs.cpSync( Config.getPackageDir() + "/jekyll", outputDir, { recursive: true } );
17
18
  // Then copy Jekyll files from current dir (which may override default files).
18
19
  fs.cpSync( "jekyll", outputDir, { recursive: true } );
19
20
 
@@ -31,34 +32,118 @@ class PageRenderer {
31
32
 
32
33
  static renderLists( outputDir ) {
33
34
 
34
- const listInfo = [
35
- { name: Config.getLabel( "native", "Native" ), filename: "list_native", include: ( t ) => t.isNative() },
36
- { name: Config.getLabel( "introduced", "Introduced" ), filename: "list_introduced", include: ( t ) => !t.isNative() },
37
- { name: "All Plants", filename: "list_all", include: () => true },
38
- ];
35
+ function getListArray( listInfo, attributes = {} ) {
36
+ const listArray = [];
37
+ for ( const list of listInfo ) {
38
+ const taxa = [];
39
+ const calfloraTaxa = [];
40
+ const iNatTaxa = [];
41
+ for ( const taxon of Taxa.getTaxa() ) {
42
+ if ( list.include( taxon ) ) {
43
+ taxa.push( taxon );
44
+ calfloraTaxa.push( taxon.getCalfloraName() );
45
+ iNatTaxa.push( taxon.getINatName() );
46
+ }
47
+ }
39
48
 
40
- const listsHTML = [];
41
- for ( const list of listInfo ) {
42
- const taxa = [];
43
- const calfloraTaxa = [];
44
- const iNatTaxa = [];
45
- for ( const taxon of Taxa.getTaxa() ) {
46
- if ( list.include( taxon ) ) {
47
- taxa.push( taxon );
48
- calfloraTaxa.push( taxon.getCalfloraName() );
49
- iNatTaxa.push( taxon.getINatName() );
49
+ if ( taxa.length === 0 ) {
50
+ continue;
50
51
  }
52
+
53
+ fs.writeFileSync( outputDir + "/calflora_" + list.filename + ".txt", calfloraTaxa.join( "\n" ) );
54
+ fs.writeFileSync( outputDir + "/inat_" + list.filename + ".txt", iNatTaxa.join( "\n" ) );
55
+
56
+ new PageTaxonList().render( outputDir, taxa, list.filename, list.name );
57
+
58
+ // Check for sublists.
59
+ const subListHTML = list.listInfo ? getListArray( list.listInfo, { class: "indent" } ) : "";
60
+
61
+ listArray.push( HTML.getLink( "./" + list.filename + ".html", list.name ) + " (" + taxa.length + ")" + subListHTML );
51
62
  }
63
+ return renderList( listArray, attributes );
64
+ }
65
+
66
+ function renderList( listsHTML, attributes = {} ) {
67
+ return HTML.getElement( "ul", HTML.arrayToLI( listsHTML ), attributes, HTML_OPTIONS.NO_ESCAPE );
68
+ }
52
69
 
53
- fs.writeFileSync( outputDir + "/calflora_" + list.filename + ".txt", calfloraTaxa.join( "\n" ) );
54
- fs.writeFileSync( outputDir + "/inat_" + list.filename + ".txt", iNatTaxa.join( "\n" ) );
70
+ function renderSection( title, listsHTML ) {
71
+ let html = "<div class=\"section\">";
72
+ html += HTML.getElement( "h2", title );
73
+ html += listsHTML;
74
+ html += "</div>";
75
+ return html;
76
+ }
77
+
78
+ const sections = [
79
+ {
80
+ title: "All Species",
81
+ listInfo: [
82
+ { name: Config.getLabel( "native", "Native" ), filename: "list_native", include: ( t ) => t.isNative() },
83
+ { name: Config.getLabel( "introduced", "Introduced" ), filename: "list_introduced", include: ( t ) => !t.isNative() },
84
+ { name: "All Plants", filename: "list_all", include: () => true },
85
+ ]
86
+ },
87
+ {
88
+ title: "Rare Plants",
89
+ listInfo: [
90
+ {
91
+ name: "CNPS Ranked Plants",
92
+ filename: "list_rpi",
93
+ include: ( t ) => t.getRPIRank() !== undefined,
94
+ listInfo: [
95
+ {
96
+ name: RPI.getRankDescription( "1A" ),
97
+ filename: "list_rpi_1a",
98
+ include: ( t ) => t.getRPIRank() === "1A",
99
+ },
100
+ {
101
+ name: RPI.getRankDescription( "1B" ),
102
+ filename: "list_rpi_1b",
103
+ include: ( t ) => t.getRPIRank() === "1B",
104
+ },
105
+ {
106
+ name: RPI.getRankDescription( "2A" ),
107
+ filename: "list_rpi_2a",
108
+ include: ( t ) => t.getRPIRank() === "2A",
109
+ },
110
+ {
111
+ name: RPI.getRankDescription( "2B" ),
112
+ filename: "list_rpi_2b",
113
+ include: ( t ) => t.getRPIRank() === "2B",
114
+ },
115
+ {
116
+ name: RPI.getRankDescription( "3" ),
117
+ filename: "list_rpi_3",
118
+ include: ( t ) => t.getRPIRank() === "3",
119
+ },
120
+ {
121
+ name: RPI.getRankDescription( "4" ),
122
+ filename: "list_rpi_4",
123
+ include: ( t ) => t.getRPIRank() === "4",
124
+ },
125
+ ]
126
+ },
127
+ ]
128
+ },
129
+ ];
130
+
131
+ let html = "<div class=\"wrapper\">";
132
+ for ( const section of sections ) {
133
+
134
+ const listHTML = getListArray( section.listInfo );
135
+
136
+ if ( listHTML.length > 0 ) {
137
+ html += renderSection( section.title, listHTML );
138
+ }
55
139
 
56
- new PageTaxonList().render( outputDir, taxa, list.filename, list.name );
57
- listsHTML.push( HTML.getLink( "./" + list.filename + ".html", list.name ) + " (" + taxa.length + ")" );
58
140
  }
141
+ html += renderSection( "Taxonomy", renderList( [ HTML.getLink( "./list_families.html", "Plant Families" ) ] ) );
142
+
143
+ html += "</div>";
59
144
 
60
145
  // Write lists to includes directory so it can be inserted into pages.
61
- fs.writeFileSync( outputDir + "/_includes/plantlists.html", "<ul>" + HTML.arrayToLI( listsHTML ) + "</ul>" );
146
+ fs.writeFileSync( outputDir + "/_includes/plantlists.html", html );
62
147
 
63
148
  }
64
149
 
package/lib/pagetaxon.js CHANGED
@@ -3,6 +3,7 @@ import { HTML, HTML_OPTIONS } from "./html.js";
3
3
  import { Jepson } from "./jepson.js";
4
4
  import { HTMLPage } from "./htmlpage.js";
5
5
  import { Config } from "./config.js";
6
+ import { RPI } from "./rpi.js";
6
7
 
7
8
  class PageTaxon extends HTMLPage {
8
9
 
@@ -91,15 +92,7 @@ class PageTaxon extends HTMLPage {
91
92
  if ( genus ) {
92
93
  const taxa = genus.getTaxa();
93
94
  for ( const taxon of taxa ) {
94
- if ( taxon.getName() !== this.#taxon.getName() ) {
95
- links.push(
96
- HTML.getLink(
97
- "./" + taxon.getFileName(),
98
- taxon.getName(),
99
- { class: taxon.getHTMLClassName() }
100
- )
101
- );
102
- }
95
+ links.push( taxon.getHTMLLink( taxon.getName() !== this.#taxon.getName() ) );
103
96
  }
104
97
  }
105
98
  return links;
@@ -126,11 +119,22 @@ class PageTaxon extends HTMLPage {
126
119
  const family = this.#taxon.getFamily();
127
120
  html += HTML.getElement(
128
121
  "div",
129
- "Family: " + HTML.getLink( "./" + family.getFileName(), family.getName() ),
122
+ HTML.getElement( "span", "Family:", { class: "label" } ) + HTML.getLink( "./" + family.getFileName(), family.getName() ),
130
123
  { class: "section" },
131
124
  HTML_OPTIONS.NO_ESCAPE
132
125
  );
133
126
 
127
+ const cnpsRank = this.#taxon.getRPIRankAndThreat();
128
+ if ( cnpsRank ) {
129
+ html += HTML.getElement(
130
+ "div",
131
+ HTML.getElement( "span", "CNPS Rare Plant Rank:", { class: "label" } )
132
+ + HTML.getElement( "span", cnpsRank, { title: RPI.getRankAndThreatDescriptions( cnpsRank ).join( "\n" ) } ),
133
+ { class: "section" },
134
+ HTML_OPTIONS.NO_ESCAPE
135
+ );
136
+ }
137
+
134
138
  html += "</div>";
135
139
 
136
140
  const introName = "intros/" + this.#taxon.getFileName( "md" );
package/lib/rpi.js ADDED
@@ -0,0 +1,30 @@
1
+ const RANK_DESCRIPS = {
2
+ "1A": "Presumed Extirpated or Extinct",
3
+ "1B": "Rare or Endangered",
4
+ "2A": "Extirpated in California",
5
+ "2B": "Rare or Endangered in California",
6
+ "3": "Needs Review",
7
+ "4": "Uncommon in California",
8
+ };
9
+
10
+ const THREAT_DESCRIPS = {
11
+ "1": "Seriously threatened in California",
12
+ "2": "Moderately threatened in California",
13
+ "3": "Not very threatened in California",
14
+ };
15
+
16
+ class RPI {
17
+
18
+ static getRankDescription( rank ) {
19
+ const pieces = rank.split( "." );
20
+ return RANK_DESCRIPS[ pieces[ 0 ] ];
21
+ }
22
+
23
+ static getRankAndThreatDescriptions( rank ) {
24
+ const pieces = rank.split( "." );
25
+ return [ RANK_DESCRIPS[ pieces[ 0 ] ], THREAT_DESCRIPS[ pieces[ 1 ] ] ];
26
+ }
27
+
28
+ }
29
+
30
+ export { RPI };
package/lib/taxa.js CHANGED
@@ -19,7 +19,7 @@ class Taxa {
19
19
  html += "<tr>";
20
20
  html += HTML.getElement(
21
21
  "td",
22
- HTML.getLink( "./" + taxon.getFileName(), taxon.getName(), { class: taxon.getHTMLClassName() } ),
22
+ taxon.getHTMLLink(),
23
23
  undefined,
24
24
  HTML_OPTIONS.NO_ESCAPE
25
25
  );
@@ -55,7 +55,15 @@ class Taxa {
55
55
  ErrorLog.log( name, "has multiple entries" );
56
56
  }
57
57
  const commonName = row[ "common name" ];
58
- this.#taxa[ name ] = new Taxon( name, commonName, status, jepsonID, row[ "calrecnum" ], row[ "inat id" ] );
58
+ this.#taxa[ name ] = new Taxon(
59
+ name,
60
+ commonName,
61
+ status,
62
+ jepsonID,
63
+ row[ "calrecnum" ],
64
+ row[ "inat id" ],
65
+ row[ "CRPR" ]
66
+ );
59
67
  if ( !jepsonID ) {
60
68
  ErrorLog.log( name, "has no Jepson ID" );
61
69
  }
package/lib/taxon.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Config } from "./config.js";
2
2
  import { ErrorLog } from "./errorlog.js";
3
3
  import { Genera } from "./genera.js";
4
+ import { RPI } from "./rpi.js";
5
+ import { HTML, HTML_OPTIONS } from "./html.js";
4
6
 
5
7
  class Taxon {
6
8
 
@@ -12,9 +14,10 @@ class Taxon {
12
14
  #calRecNum;
13
15
  #iNatID;
14
16
  #iNatName;
17
+ #rankRPI;
15
18
  #synonyms = [];
16
19
 
17
- constructor( name, commonNames, status, jepsonID, calRecNum, iNatID ) {
20
+ constructor( name, commonNames, status, jepsonID, calRecNum, iNatID, rankRPI ) {
18
21
  this.#name = name;
19
22
  this.#genus = name.split( " " )[ 0 ];
20
23
  this.#commonNames = commonNames ? commonNames.split( "," ).map( t => t.trim() ) : [];
@@ -22,6 +25,7 @@ class Taxon {
22
25
  this.#jepsonID = jepsonID;
23
26
  this.#calRecNum = calRecNum;
24
27
  this.#iNatID = iNatID;
28
+ this.#rankRPI = rankRPI;
25
29
  Genera.addTaxon( this );
26
30
  if ( !calRecNum ) {
27
31
  ErrorLog.log( this.getName(), "has no Calflora ID" );
@@ -63,14 +67,20 @@ class Taxon {
63
67
  return Genera.getGenus( this.#genus );
64
68
  }
65
69
 
66
- getHTMLClassName() {
67
- return this.isNative() ? "native" : "non-native";
68
- }
69
-
70
70
  getGenusName() {
71
71
  return this.#genus;
72
72
  }
73
73
 
74
+ getHTMLLink( href = true ) {
75
+ href = href ? ( "./" + this.getFileName() ) : undefined;
76
+ const className = this.isNative() ? ( this.isRare() ? "rare" : "native" ) : "non-native";
77
+ const attributes = { class: className };
78
+ if ( className === "rare" ) {
79
+ attributes[ "title" ] = RPI.getRankAndThreatDescriptions( this.getRPIRankAndThreat() ).join( "\n" );
80
+ }
81
+ return HTML.getElement( "span", HTML.getLink( href, this.getName() ), attributes, HTML_OPTIONS.NO_ESCAPE );
82
+ }
83
+
74
84
  getINatID() {
75
85
  return this.#iNatID;
76
86
  }
@@ -88,6 +98,17 @@ class Taxon {
88
98
  return this.#name;
89
99
  }
90
100
 
101
+ getRPIRank() {
102
+ if ( !this.#rankRPI ) {
103
+ return this.#rankRPI;
104
+ }
105
+ return this.#rankRPI.split( "." )[ 0 ];
106
+ }
107
+
108
+ getRPIRankAndThreat() {
109
+ return this.#rankRPI;
110
+ }
111
+
91
112
  getStatus() {
92
113
  return this.#status;
93
114
  }
@@ -103,6 +124,7 @@ class Taxon {
103
124
  }
104
125
  throw new Error( this.#status );
105
126
  }
127
+
106
128
  getSynonyms() {
107
129
  return this.#synonyms;
108
130
  }
@@ -111,6 +133,10 @@ class Taxon {
111
133
  return this.#status === "N";
112
134
  }
113
135
 
136
+ isRare() {
137
+ return this.getRPIRank() !== undefined;
138
+ }
139
+
114
140
  }
115
141
 
116
142
  export { Taxon };
package/package.json CHANGED
@@ -1,9 +1,28 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Tools to create Jekyll files for a website listing plants in an area of California.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ca-plants/ca-plant-list.git"
9
+ },
10
+ "homepage": "https://github.com/ca-plants/ca-plant-list",
6
11
  "type": "module",
12
+ "exports": {
13
+ "./CSV": {
14
+ "import": "./lib/csv.js"
15
+ },
16
+ "./DataLoader": {
17
+ "import": "./lib/dataloader.js"
18
+ },
19
+ "./Exceptions": {
20
+ "import": "./lib/exceptions.js"
21
+ },
22
+ "./Taxa": {
23
+ "import": "./lib/taxa.js"
24
+ }
25
+ },
7
26
  "bin": {
8
27
  "ca-plant-list": "build-site.js"
9
28
  },
@@ -14,4 +33,4 @@
14
33
  "devDependencies": {
15
34
  "eslint": "^8.26.0"
16
35
  }
17
- }
36
+ }