@ca-plant-list/ca-plant-list 0.1.2 → 0.1.4

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.
File without changes
@@ -29,7 +29,7 @@
29
29
  <a class="nav-link" href="{{site.baseurl}}/rare_plants.html">Rare Plants</a>
30
30
  </li>
31
31
  <li class="nav-item">
32
- <a class="nav-link" href="{{site.baseurl}}/common_name_search.html">Common Name Search</a>
32
+ <a class="nav-link" href="{{site.baseurl}}/name_search.html">Name Search</a>
33
33
  </li>
34
34
  {%include menu_extra.html %}
35
35
  </ul>
@@ -28,6 +28,7 @@ span.label {
28
28
 
29
29
  td {
30
30
  border: solid 1px;
31
+ padding: 0 .5rem;
31
32
  vertical-align: top;
32
33
  }
33
34
 
@@ -85,4 +86,9 @@ div.section li {
85
86
 
86
87
  div.section ul.indent {
87
88
  padding-left: 1rem;
89
+ }
90
+
91
+ /* Forms */
92
+ input {
93
+ margin-right: .5em;
88
94
  }
@@ -0,0 +1,156 @@
1
+ const MIN_LEN = 2;
2
+ const MAX_RESULTS = 50;
3
+
4
+ class Search {
5
+
6
+ static #debounceTimer;
7
+ static #searchData;
8
+
9
+ static #debounce( func ) {
10
+ clearTimeout( this.#debounceTimer );
11
+ this.#debounceTimer = setTimeout( func, 500 );
12
+ }
13
+
14
+ static #doSearch() {
15
+
16
+ function matchTaxon( taxon, value ) {
17
+
18
+ function matchSynonyms( syns, value ) {
19
+ const matchedIndexes = [];
20
+ if ( syns ) {
21
+ for ( let index = 0; index < syns.length; index++ ) {
22
+ if ( syns[ index ].includes( value ) ) {
23
+ matchedIndexes.push( index );
24
+ }
25
+ }
26
+ }
27
+ return matchedIndexes;
28
+ }
29
+
30
+ const rawData = taxon[ 0 ];
31
+ const name = taxon[ 1 ];
32
+ const cn = taxon[ 2 ];
33
+ const syns = matchSynonyms( taxon[ 3 ], value );
34
+ if ( syns.length > 0 ) {
35
+ // Include any matching synonyms.
36
+ for ( const index of syns ) {
37
+ matches.push( [ rawData[ 0 ], rawData[ 1 ], rawData[ 2 ][ index ] ] );
38
+ }
39
+ } else {
40
+ // No synonyms match; see if the scientific or common names match.
41
+ const namesMatch = name.includes( value ) || ( cn && cn.includes( value ) );
42
+ if ( namesMatch ) {
43
+ matches.push( [ rawData[ 0 ], rawData[ 1 ] ] );
44
+ }
45
+ }
46
+
47
+ }
48
+
49
+ Search.#debounceTimer = undefined;
50
+
51
+ const value = document.getElementById( "name" ).value.toLowerCase();
52
+
53
+ const matches = [];
54
+ const shouldSearch = ( value.length >= MIN_LEN );
55
+
56
+ if ( shouldSearch ) {
57
+
58
+ // If the search data is not done generating, try again later.
59
+ if ( !Search.#searchData ) {
60
+ this.#debounce( Search.#doSearch );
61
+ }
62
+
63
+ for ( const taxon of Search.#searchData ) {
64
+ matchTaxon( taxon, value );
65
+ }
66
+ }
67
+
68
+ const eBody = document.createElement( "tbody" );
69
+ if ( matches.length <= MAX_RESULTS ) {
70
+ for ( const match of matches ) {
71
+
72
+ const tr = document.createElement( "tr" );
73
+
74
+ // Scientific name.
75
+ const name = match[ 0 ];
76
+ const syn = match[ 2 ];
77
+ const td1 = document.createElement( "td" );
78
+ const link = document.createElement( "a" );
79
+ link.setAttribute( "href", "./" + name.replaceAll( ".", "" ).replaceAll( " ", "-" ) + ".html" );
80
+ link.textContent = name;
81
+ td1.appendChild( link );
82
+ if ( syn ) {
83
+ td1.appendChild( document.createTextNode( " (" + syn + ")" ) );
84
+ }
85
+ tr.appendChild( td1 );
86
+
87
+ const cn = match[ 1 ];
88
+ const td2 = document.createElement( "td" );
89
+ if ( cn ) {
90
+ td2.textContent = cn;
91
+ }
92
+ tr.appendChild( td2 );
93
+
94
+ eBody.appendChild( tr );
95
+
96
+ }
97
+ }
98
+
99
+ // Delete current message
100
+ const eMessage = document.getElementById( "message" );
101
+ if ( eMessage.firstChild ) {
102
+ eMessage.removeChild( eMessage.firstChild );
103
+ }
104
+ if ( shouldSearch ) {
105
+ if ( matches.length === 0 ) {
106
+ eMessage.textContent = "Nothing found.";
107
+ }
108
+ if ( matches.length > MAX_RESULTS ) {
109
+ eMessage.textContent = "Too many results.";
110
+ }
111
+ }
112
+
113
+ // Delete current results
114
+ const eTable = document.getElementById( "results" );
115
+ if ( eTable.firstChild ) {
116
+ eTable.removeChild( eTable.firstChild );
117
+ }
118
+
119
+ eTable.appendChild( eBody );
120
+ }
121
+
122
+ static async generateSearchData() {
123
+ const searchData = [];
124
+ // eslint-disable-next-line no-undef
125
+ for ( const taxon of NAMES ) {
126
+ const taxonData = [ taxon ];
127
+ taxonData.push( taxon[ 0 ].toLowerCase().replace( / (subsp|var)\./, "" ) );
128
+ if ( taxon[ 1 ] ) {
129
+ taxonData.push( taxon[ 1 ].toLowerCase() );
130
+ }
131
+ if ( taxon[ 2 ] ) {
132
+ const syns = [];
133
+ for ( const syn of taxon[ 2 ] ) {
134
+ syns.push( syn.toLowerCase().replace( / (subsp|var)\./, "" ) );
135
+ }
136
+ taxonData[ 3 ] = syns;
137
+ }
138
+ searchData.push( taxonData );
139
+ }
140
+ this.#searchData = searchData;
141
+ }
142
+
143
+ static #handleChange() {
144
+ this.#debounce( Search.#doSearch );
145
+ }
146
+
147
+ static init() {
148
+ this.generateSearchData();
149
+ const eName = document.getElementById( "name" );
150
+ eName.focus();
151
+ eName.oninput = ( ev ) => { return this.#handleChange( ev ); };
152
+ }
153
+
154
+ }
155
+
156
+ Search.init();
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Name Search
3
+ js: name_search.js
4
+ ---
5
+ <h1>Search</h1>
6
+
7
+ <script>
8
+ const NAMES = {% include names.json%};
9
+ </script>
10
+
11
+ <form>
12
+ <input type="text" id="name"><label for="name">Search for scientific name, common name, or synonym</label>
13
+ </form>
14
+
15
+ <div class="section" id="message"></div>
16
+
17
+ <table id="results">
18
+ </table>
@@ -0,0 +1,46 @@
1
+ import * as fs from "node:fs";
2
+ import { Config } from "./config.js";
3
+
4
+ class BasePageRenderer {
5
+
6
+ static render( outputDir, Taxa ) {
7
+
8
+ // Copy static files
9
+ fs.rmSync( outputDir, { force: true, recursive: true, maxRetries: 2, retryDelay: 200 } );
10
+ // First copy default Jekyll files from package.
11
+ fs.cpSync( Config.getPackageDir() + "/jekyll", outputDir, { recursive: true } );
12
+ // Then copy Jekyll files from current dir (which may override default files).
13
+ fs.cpSync( "jekyll", outputDir, { recursive: true } );
14
+
15
+ this.renderTools( outputDir, Taxa );
16
+
17
+ }
18
+
19
+ static renderTools( outputDir, Taxa ) {
20
+
21
+ const names = [];
22
+ for ( const taxon of Taxa.getTaxa() ) {
23
+ const row = [];
24
+ row.push( taxon.getName() );
25
+ const cn = taxon.getCommonNames().join( "," );
26
+ if ( cn ) {
27
+ row.push( cn );
28
+ }
29
+ const synonyms = [];
30
+ for ( const syn of taxon.getSynonyms() ) {
31
+ synonyms.push( syn );
32
+ }
33
+ if ( synonyms.length > 0 ) {
34
+ row[ 2 ] = synonyms;
35
+ }
36
+ names.push( row );
37
+ }
38
+
39
+ fs.writeFileSync( outputDir + "/_includes/names.json", JSON.stringify( names ) );
40
+
41
+ }
42
+
43
+
44
+ }
45
+
46
+ export { BasePageRenderer };
package/lib/dataloader.js CHANGED
@@ -14,16 +14,22 @@ class DataLoader {
14
14
  return OPTION_DEFS;
15
15
  }
16
16
 
17
- static load( options ) {
17
+ static init( taxaDir ) {
18
18
 
19
19
  const defaultDataDir = Config.getPackageDir() + "/data";
20
- const taxaDir = options.datadir;
21
-
22
- console.log( "loading data" );
23
20
 
24
21
  Exceptions.init( taxaDir );
25
22
  Families.init( defaultDataDir );
26
23
  Genera.init( defaultDataDir );
24
+ }
25
+
26
+ static load( options ) {
27
+
28
+ const taxaDir = options.datadir;
29
+
30
+ console.log( "loading data" );
31
+
32
+ this.init( taxaDir );
27
33
  Taxa.init( taxaDir );
28
34
 
29
35
  }
package/lib/exceptions.js CHANGED
@@ -15,14 +15,16 @@ class Exceptions {
15
15
  return false;
16
16
  }
17
17
 
18
- static getValue( name, cat, subcat ) {
18
+ static getValue( name, cat, subcat, defaultValue ) {
19
19
  const taxonData = this.#exceptions[ name ];
20
20
  if ( taxonData ) {
21
21
  const catData = taxonData[ cat ];
22
22
  if ( catData ) {
23
- return catData[ subcat ];
23
+ const val = catData[ subcat ];
24
+ return ( val === undefined ) ? defaultValue : val;
24
25
  }
25
26
  }
27
+ return defaultValue;
26
28
  }
27
29
 
28
30
  static init( dir ) {
package/lib/files.js ADDED
@@ -0,0 +1,17 @@
1
+ import * as fs from "node:fs";
2
+
3
+ class Files {
4
+
5
+ static async fetch( url, targetFileName ) {
6
+ const response = await fetch( url );
7
+ const data = await response.text();
8
+ fs.writeFileSync( targetFileName, data );
9
+ }
10
+
11
+ static read( path ) {
12
+ return fs.readFileSync( path, "utf8" );
13
+ }
14
+
15
+ }
16
+
17
+ export { Files };
package/lib/genera.js CHANGED
@@ -9,6 +9,11 @@ class Genera {
9
9
 
10
10
  const genusName = taxon.getGenusName();
11
11
  const genusData = this.#genera[ genusName ];
12
+ if ( !genusData ) {
13
+ console.log( taxon.getName() + " genus not found" );
14
+ return;
15
+ }
16
+
12
17
  if ( genusData.taxa === undefined ) {
13
18
  genusData.taxa = [];
14
19
  }
@@ -16,7 +21,7 @@ class Genera {
16
21
 
17
22
  const family = this.getFamily( genusName );
18
23
  if ( !family ) {
19
- console.log( taxon.getName() + " genus/family not found" );
24
+ console.log( taxon.getName() + " family not found" );
20
25
  return;
21
26
  }
22
27
  family.addTaxon( taxon );
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { BasePageRenderer } from "./basepagerenderer.js";
2
+ import { Config } from "./config.js";
3
+ import { CSV } from "./csv.js";
4
+ import { DataLoader } from "./dataloader.js";
5
+ import { ErrorLog } from "./errorlog.js";
6
+ import { Exceptions } from "./exceptions.js";
7
+ import { HTML, HTML_OPTIONS } from "./html.js";
8
+ import { Taxon } from "./taxon.js";
9
+
10
+ export { BasePageRenderer, Config, CSV, DataLoader, ErrorLog, Exceptions, HTML, HTML_OPTIONS, Taxon };
@@ -6,20 +6,15 @@ import { HTMLPage } from "./htmlpage.js";
6
6
  import { PageTaxon } from "./pagetaxon.js";
7
7
  import { Config } from "./config.js";
8
8
  import { RarePlants } from "./rareplants.js";
9
+ import { BasePageRenderer } from "./basepagerenderer.js";
9
10
 
10
- class PageRenderer {
11
+ class PageRenderer extends BasePageRenderer {
11
12
 
12
13
  static render( outputDir ) {
13
14
 
14
- // Copy static files
15
- fs.rmSync( outputDir, { force: true, recursive: true } );
16
- // First copy default Jekyll files from package.
17
- fs.cpSync( Config.getPackageDir() + "/jekyll", outputDir, { recursive: true } );
18
- // Then copy Jekyll files from current dir (which may override default files).
19
- fs.cpSync( "jekyll", outputDir, { recursive: true } );
15
+ super.render( outputDir, Taxa );
20
16
 
21
17
  this.renderLists( outputDir );
22
- this.renderTools( outputDir );
23
18
 
24
19
  Families.renderPages( outputDir );
25
20
 
@@ -174,30 +169,6 @@ class PageRenderer {
174
169
 
175
170
  }
176
171
 
177
- static renderTools( outputDir ) {
178
-
179
- const commonNames = [];
180
- for ( const taxon of Taxa.getTaxa() ) {
181
- for ( const commonName of taxon.getCommonNames() ) {
182
- commonNames.push( [ commonName, taxon.getName() ] );
183
- }
184
- }
185
-
186
- const cnObj = {};
187
- for ( const commonName of commonNames.sort( ( a, b ) => a[ 0 ].localeCompare( b[ 0 ] ) ) ) {
188
- const normalizedName = commonName[ 0 ].toLowerCase().replaceAll( "-", " " ).replaceAll( "'", "" );
189
- let data = cnObj[ normalizedName ];
190
- if ( !data ) {
191
- data = { cn: commonName[ 0 ], names: [] };
192
- cnObj[ normalizedName ] = data;
193
- }
194
- data.names.push( commonName[ 1 ] );
195
- }
196
- fs.writeFileSync( outputDir + "/_includes/common_names.json", JSON.stringify( cnObj ) );
197
-
198
-
199
- }
200
-
201
172
  }
202
173
 
203
174
  class PageTaxonList extends HTMLPage {
package/lib/pagetaxon.js CHANGED
@@ -31,16 +31,9 @@ class PageTaxon extends HTMLPage {
31
31
  )
32
32
  );
33
33
  }
34
- const iNatID = this.#taxon.getINatID();
35
- if ( iNatID ) {
36
- links.push(
37
- HTML.getLink(
38
- "https://www.inaturalist.org/taxa/" + iNatID,
39
- "iNaturalist",
40
- {},
41
- HTML_OPTIONS.OPEN_NEW
42
- )
43
- );
34
+ const iNatLink = this.#taxon.getINatTaxonLink();
35
+ if ( iNatLink ) {
36
+ links.push( iNatLink );
44
37
  }
45
38
  return links;
46
39
  }
package/lib/taxa.js CHANGED
@@ -62,6 +62,7 @@ class Taxa {
62
62
  jepsonID,
63
63
  row[ "calrecnum" ],
64
64
  row[ "inat id" ],
65
+ row[ "RPI ID" ],
65
66
  row[ "CRPR" ],
66
67
  row[ "CESA" ]
67
68
  );
package/lib/taxon.js CHANGED
@@ -12,13 +12,15 @@ class Taxon {
12
12
  #status;
13
13
  #jepsonID;
14
14
  #calRecNum;
15
+ #cfSyn;
15
16
  #iNatID;
16
17
  #iNatSyn;
18
+ #rpiID;
17
19
  #rankRPI;
18
20
  #cesa;
19
21
  #synonyms = [];
20
22
 
21
- constructor( name, commonNames, status, jepsonID, calRecNum, iNatID, rankRPI, cesa ) {
23
+ constructor( name, commonNames, status, jepsonID, calRecNum, iNatID, rpiID, rankRPI, cesa ) {
22
24
  this.#name = name;
23
25
  this.#genus = name.split( " " )[ 0 ];
24
26
  this.#commonNames = commonNames ? commonNames.split( "," ).map( t => t.trim() ) : [];
@@ -26,6 +28,7 @@ class Taxon {
26
28
  this.#jepsonID = jepsonID;
27
29
  this.#calRecNum = calRecNum;
28
30
  this.#iNatID = iNatID;
31
+ this.#rpiID = rpiID;
29
32
  this.#rankRPI = rankRPI;
30
33
  this.#cesa = cesa;
31
34
  Genera.addTaxon( this );
@@ -39,14 +42,23 @@ class Taxon {
39
42
 
40
43
  addSynonym( syn, type ) {
41
44
  this.#synonyms.push( syn );
42
- if ( type === "INAT" ) {
43
- // Synonyms should be in Jepson format, but store iNatName in iNat format (no var or subsp, space after x).
44
- this.#iNatSyn = syn;
45
+ switch ( type ) {
46
+ case "CF":
47
+ // Synonym is in Calflora format.
48
+ this.#cfSyn = syn;
49
+ break;
50
+ case "INAT":
51
+ // Synonyms should be in Jepson format, but store iNatName in iNat format (no var or subsp, space after x).
52
+ this.#iNatSyn = syn;
53
+ break;
45
54
  }
46
55
  }
47
56
 
48
57
  getCalfloraName() {
49
- return this.#name.replace( " subsp.", " ssp." ).replace( "×", "X" );
58
+ if ( this.#cfSyn ) {
59
+ return this.#cfSyn;
60
+ }
61
+ return this.getName().replace( " subsp.", " ssp." ).replace( "×", "X" );
50
62
  }
51
63
 
52
64
  getCalfloraID() {
@@ -65,9 +77,13 @@ class Taxon {
65
77
  return Genera.getFamily( this.#genus );
66
78
  }
67
79
 
68
- getFileName( ext = "html" ) {
80
+ getFileName( ext ) {
81
+ return Taxon.getFileName( this.getName(), ext );
82
+ }
83
+
84
+ static getFileName( name, ext = "html" ) {
69
85
  // Convert spaces to "-" and remove ".".
70
- return this.getName().replaceAll( " ", "-" ).replaceAll( ".", "" ) + "." + ext;
86
+ return name.replaceAll( " ", "-" ).replaceAll( ".", "" ) + "." + ext;
71
87
  }
72
88
 
73
89
  getGenus() {
@@ -97,6 +113,15 @@ class Taxon {
97
113
  return name.replace( / (subsp|var)\./, "" ).replace( "×", "× " );
98
114
  }
99
115
 
116
+ getINatTaxonLink() {
117
+ const iNatID = this.getINatID();
118
+ if ( !iNatID ) {
119
+ return "";
120
+ }
121
+ const link = HTML.getLink( "https://www.inaturalist.org/taxa/" + iNatID, "iNaturalist", {}, HTML_OPTIONS.OPEN_NEW );
122
+ return this.#iNatSyn ? ( link + " (" + this.#iNatSyn + ")" ) : link;
123
+ }
124
+
100
125
  getJepsonID() {
101
126
  return this.#jepsonID;
102
127
  }
@@ -105,6 +130,10 @@ class Taxon {
105
130
  return this.#name;
106
131
  }
107
132
 
133
+ getRPIID() {
134
+ return this.#rpiID;
135
+ }
136
+
108
137
  getRPIRank() {
109
138
  if ( !this.#rankRPI ) {
110
139
  return this.#rankRPI;
@@ -144,6 +173,18 @@ class Taxon {
144
173
  return this.getRPIRank() !== undefined;
145
174
  }
146
175
 
176
+ setCalfloraID( id ) {
177
+ this.#calRecNum = id;
178
+ }
179
+
180
+ setINatID( id ) {
181
+ this.#iNatID = id;
182
+ }
183
+
184
+ setJepsonID( id ) {
185
+ this.#jepsonID = id;
186
+ }
187
+
147
188
  }
148
189
 
149
190
  export { Taxon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ca-plant-list/ca-plant-list",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": {
@@ -10,21 +10,7 @@
10
10
  "homepage": "https://github.com/ca-plants/ca-plant-list",
11
11
  "type": "module",
12
12
  "exports": {
13
- "./Config": {
14
- "import": "./lib/config.js"
15
- },
16
- "./CSV": {
17
- "import": "./lib/csv.js"
18
- },
19
- "./DataLoader": {
20
- "import": "./lib/dataloader.js"
21
- },
22
- "./Exceptions": {
23
- "import": "./lib/exceptions.js"
24
- },
25
- "./Taxa": {
26
- "import": "./lib/taxa.js"
27
- }
13
+ ".": "./lib/index.js"
28
14
  },
29
15
  "bin": {
30
16
  "ca-plant-list": "build-site.js"
@@ -1,87 +0,0 @@
1
- const MIN_LEN = 2;
2
- const MAX_RESULTS = 50;
3
-
4
- class Search {
5
-
6
- static #debounceTimer;
7
-
8
- static #debounce( func ) {
9
- clearTimeout( this.#debounceTimer );
10
- this.#debounceTimer = setTimeout( func, 500 );
11
- }
12
-
13
- static #doSearch() {
14
-
15
- Search.#debounceTimer = undefined;
16
-
17
- const value = document.getElementById( "common-name" ).value.toLowerCase();
18
-
19
- const matches = [];
20
- const shouldSearch = ( value.length >= MIN_LEN );
21
-
22
- if ( shouldSearch ) {
23
- // eslint-disable-next-line no-undef
24
- for ( const key of Object.keys( COMMON_NAMES ) ) {
25
- if ( key.includes( value ) ) {
26
- matches.push( key );
27
- }
28
- }
29
- }
30
-
31
- const eBody = document.createElement( "tbody" );
32
- if ( matches.length <= MAX_RESULTS ) {
33
- for ( const match of matches ) {
34
- // eslint-disable-next-line no-undef
35
- const data = COMMON_NAMES[ match ];
36
- for ( const name of data.names ) {
37
- const tr = document.createElement( "tr" );
38
- const td1 = document.createElement( "td" );
39
- const td2 = document.createElement( "td" );
40
- const link = document.createElement( "a" );
41
- td1.textContent = data.cn;
42
- link.setAttribute( "href", "./" + name.replaceAll( ".", "" ).replaceAll( " ", "-" ) + ".html" );
43
- link.textContent = name;
44
- td2.appendChild( link );
45
- tr.appendChild( td1 );
46
- tr.appendChild( td2 );
47
- eBody.appendChild( tr );
48
- }
49
- }
50
- }
51
-
52
- // Delete current message
53
- const eMessage = document.getElementById( "message" );
54
- if ( eMessage.firstChild ) {
55
- eMessage.removeChild( eMessage.firstChild );
56
- }
57
- if ( shouldSearch ) {
58
- if ( matches.length === 0 ) {
59
- eMessage.textContent = "Nothing found.";
60
- }
61
- if ( matches.length > MAX_RESULTS ) {
62
- eMessage.textContent = "Too many results.";
63
- }
64
- }
65
-
66
- // Delete current results
67
- const eTable = document.getElementById( "results" );
68
- if ( eTable.firstChild ) {
69
- eTable.removeChild( eTable.firstChild );
70
- }
71
-
72
- eTable.appendChild( eBody );
73
- }
74
-
75
- static #handleChange() {
76
- this.#debounce( Search.#doSearch );
77
- }
78
-
79
- static init() {
80
- const eName = document.getElementById( "common-name" );
81
- eName.focus();
82
- eName.oninput = ( ev ) => { return this.#handleChange( ev ); };
83
- }
84
-
85
- }
86
-
87
- Search.init();
@@ -1,18 +0,0 @@
1
- ---
2
- title: Common Name Search
3
- js: common_name_search.js
4
- ---
5
- <h1>Search for Common Name</h1>
6
-
7
- <script>
8
- const COMMON_NAMES = {% include common_names.json%};
9
- </script>
10
-
11
- <form>
12
- <input type="text" id="common-name">
13
- </form>
14
-
15
- <div class="section" id="message"></div>
16
-
17
- <table id="results">
18
- </table>