@ca-plant-list/ca-plant-list 0.0.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.
@@ -0,0 +1,120 @@
1
+ import * as fs from "node:fs";
2
+ import { Families } from "./families.js";
3
+ import { HTML } from "./html.js";
4
+ import { Taxa } from "./taxa.js";
5
+ import { HTMLPage } from "./htmlpage.js";
6
+ import { PageTaxon } from "./pagetaxon.js";
7
+ import { Config } from "./config.js";
8
+
9
+ class PageRenderer {
10
+
11
+ static render( packageDir, outputDir ) {
12
+
13
+ // Copy static files
14
+ fs.rmSync( outputDir, { force: true, recursive: true } );
15
+ // First copy default Jekyll files from package.
16
+ fs.cpSync( packageDir + "/jekyll", outputDir, { recursive: true } );
17
+ // Then copy Jekyll files from current dir (which may override default files).
18
+ fs.cpSync( "jekyll", outputDir, { recursive: true } );
19
+
20
+ this.renderLists( outputDir );
21
+ this.renderTools( outputDir );
22
+
23
+ Families.renderPages( outputDir );
24
+
25
+ const taxa = Taxa.getTaxa();
26
+ for ( const taxon of taxa ) {
27
+ new PageTaxon( taxon ).render( outputDir );
28
+ }
29
+
30
+ }
31
+
32
+ static renderLists( outputDir ) {
33
+
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
+ ];
39
+
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() );
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
+ listsHTML.push( HTML.getLink( "./" + list.filename + ".html", list.name ) + " (" + taxa.length + ")" );
58
+ }
59
+
60
+ // Write lists to includes directory so it can be inserted into pages.
61
+ fs.writeFileSync( outputDir + "/_includes/plantlists.html", "<ul>" + HTML.arrayToLI( listsHTML ) + "</ul>" );
62
+
63
+ }
64
+
65
+ static renderTools( outputDir ) {
66
+
67
+ const commonNames = [];
68
+ for ( const taxon of Taxa.getTaxa() ) {
69
+ for ( const commonName of taxon.getCommonNames() ) {
70
+ commonNames.push( [ commonName, taxon.getName() ] );
71
+ }
72
+ }
73
+
74
+ const cnObj = {};
75
+ for ( const commonName of commonNames.sort( ( a, b ) => a[ 0 ].localeCompare( b[ 0 ] ) ) ) {
76
+ const normalizedName = commonName[ 0 ].toLowerCase().replaceAll( "-", " " ).replaceAll( "'", "" );
77
+ let data = cnObj[ normalizedName ];
78
+ if ( !data ) {
79
+ data = { cn: commonName[ 0 ], names: [] };
80
+ cnObj[ normalizedName ] = data;
81
+ }
82
+ data.names.push( commonName[ 1 ] );
83
+ }
84
+ fs.writeFileSync( outputDir + "/_includes/common_names.json", JSON.stringify( cnObj ) );
85
+
86
+
87
+ }
88
+
89
+ }
90
+
91
+ class PageTaxonList extends HTMLPage {
92
+
93
+ render( outputDir, taxa, baseName, title ) {
94
+
95
+ let html = this.getFrontMatter( title );
96
+
97
+ html += HTML.getElement( "h1", title );
98
+
99
+ html += "<div class=\"wrapper\">";
100
+
101
+ html += "<div class=\"section\">";
102
+ html += Taxa.getHTMLTable( taxa );
103
+ html += "</div>";
104
+
105
+ html += "<div class=\"section\">";
106
+ html += HTML.getElement( "h2", "Download" );
107
+ html += "<ul>";
108
+ html += "<li>" + HTML.getLink( "./calflora_" + baseName + ".txt", "Calflora List" ) + "</li>";
109
+ html += "<li>" + HTML.getLink( "./inat_" + baseName + ".txt", "iNaturalist List" ) + "</li>";
110
+ html += "</ul>";
111
+ html += "</div>";
112
+
113
+ html += "</div>";
114
+
115
+ this.writeFile( outputDir, baseName + ".html", html );
116
+
117
+ }
118
+ }
119
+
120
+ export { PageRenderer };
@@ -0,0 +1,154 @@
1
+ import * as fs from "node:fs";
2
+ import { HTML, HTML_OPTIONS } from "./html.js";
3
+ import { Jepson } from "./jepson.js";
4
+ import { HTMLPage } from "./htmlpage.js";
5
+ import { Config } from "./config.js";
6
+
7
+ class PageTaxon extends HTMLPage {
8
+
9
+ #taxon;
10
+
11
+ constructor( taxon ) {
12
+ super();
13
+ this.#taxon = taxon;
14
+ }
15
+
16
+ #getInfoLinks() {
17
+ const links = [];
18
+ const jepsonID = this.#taxon.getJepsonID();
19
+ if ( jepsonID ) {
20
+ links.push( Jepson.getEFloraLink( jepsonID ) );
21
+ }
22
+ const calRecNum = this.#taxon.getCalRecNum();
23
+ if ( calRecNum ) {
24
+ links.push(
25
+ HTML.getLink(
26
+ "https://www.calflora.org/app/taxon?crn=" + calRecNum,
27
+ "Calflora",
28
+ {},
29
+ HTML_OPTIONS.OPEN_NEW
30
+ )
31
+ );
32
+ }
33
+ const iNatID = this.#taxon.getINatID();
34
+ if ( iNatID ) {
35
+ links.push(
36
+ HTML.getLink(
37
+ "https://www.inaturalist.org/taxa/" + iNatID,
38
+ "iNaturalist",
39
+ {},
40
+ HTML_OPTIONS.OPEN_NEW
41
+ )
42
+ );
43
+ }
44
+ return links;
45
+ }
46
+
47
+ #getObsLinks() {
48
+ const links = [];
49
+ links.push(
50
+ HTML.getLink(
51
+ "https://www.calflora.org/entry/observ.html?track=m#srch=t&grezc=5&cols=b&lpcli=t&cc="
52
+ + Config.getConfigValue( "calflora", "counties" ).join( "!" ) + "&incobs=f&taxon="
53
+ + this.#taxon.getCalfloraName().replaceAll( " ", "+" ),
54
+ "Calflora",
55
+ {},
56
+ HTML_OPTIONS.OPEN_NEW
57
+ )
58
+ );
59
+ const iNatID = this.#taxon.getINatID();
60
+ if ( iNatID ) {
61
+ links.push(
62
+ HTML.getLink(
63
+ "https://www.inaturalist.org/observations?project_id=" + Config.getConfigValue( "inat", "project" )
64
+ + "&quality_grade=research&subview=map&taxon_id=" + iNatID,
65
+ "iNaturalist",
66
+ {},
67
+ HTML_OPTIONS.OPEN_NEW
68
+ )
69
+ );
70
+ }
71
+
72
+ return links;
73
+ }
74
+
75
+ #getListSectionHTML( list, header, className ) {
76
+ let html = "";
77
+ if ( list.length > 0 ) {
78
+ html += "<div class=\"section " + className + "\">";
79
+ html += HTML.getElement( "h2", header );
80
+ html += "<ul>";
81
+ html += HTML.arrayToLI( list );
82
+ html += "</ul>";
83
+ html += "</div>";
84
+ }
85
+ return html;
86
+ }
87
+
88
+ #getRelatedTaxaLinks() {
89
+ const links = [];
90
+ const genus = this.#taxon.getGenus();
91
+ if ( genus ) {
92
+ const taxa = genus.getTaxa();
93
+ 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
+ }
103
+ }
104
+ }
105
+ return links;
106
+ }
107
+
108
+ #getSynonyms() {
109
+ return this.#taxon.getSynonyms();
110
+ }
111
+
112
+ render( outputDir ) {
113
+
114
+ let html = this.getFrontMatter( this.#taxon.getName() );
115
+
116
+ html += HTML.getElement( "h1", this.#taxon.getName() );
117
+
118
+ html += "<div class=\"wrapper\">";
119
+
120
+ const cn = this.#taxon.getCommonNames();
121
+ if ( cn.length > 0 ) {
122
+ html += HTML.getElement( "div", cn.join( ", " ), { class: "section common-names" } );
123
+ }
124
+ html += HTML.getElement( "div", this.#taxon.getStatusDescription(), { class: "section" } );
125
+
126
+ const family = this.#taxon.getFamily();
127
+ html += HTML.getElement(
128
+ "div",
129
+ "Family: " + HTML.getLink( "./" + family.getFileName(), family.getName() ),
130
+ { class: "section" },
131
+ HTML_OPTIONS.NO_ESCAPE
132
+ );
133
+
134
+ html += "</div>";
135
+
136
+ const introName = "intros/" + this.#taxon.getFileName( "md" );
137
+ if ( fs.existsSync( "./jekyll/_includes/" + introName ) ) {
138
+ html += HTML.getElement( "div", "{% capture my_include %}{% include " + introName + "%}{% endcapture %}{{ my_include | markdownify }}" );
139
+ }
140
+
141
+ html += "<div class=\"wrapper\">";
142
+ html += this.#getListSectionHTML( this.#getInfoLinks(), "Information", "info" );
143
+ html += this.#getListSectionHTML( this.#getObsLinks(), "Observations", "obs" );
144
+ html += this.#getListSectionHTML( this.#getRelatedTaxaLinks(), "Related Species", "rel-taxa" );
145
+ html += this.#getListSectionHTML( this.#getSynonyms(), "Synonyms", "synonyms" );
146
+ html += "</div>";
147
+
148
+ this.writeFile( outputDir, this.#taxon.getFileName(), html );
149
+
150
+ }
151
+
152
+ }
153
+
154
+ export { PageTaxon };
package/lib/taxa.js ADDED
@@ -0,0 +1,86 @@
1
+ import { Taxon } from "./taxon.js";
2
+ import { ErrorLog } from "./errorlog.js";
3
+ import { HTML, HTML_OPTIONS } from "./html.js";
4
+ import { CSV } from "./csv.js";
5
+
6
+ class Taxa {
7
+
8
+ static #taxa = {};
9
+ static #sortedTaxa;
10
+
11
+ static getHTMLTable( taxa ) {
12
+ let html = "<table><thead>";
13
+ html += HTML.getElement( "th", "Species" );
14
+ html += HTML.getElement( "th", "Common Name" );
15
+ html += "</thead>";
16
+ html += "<tbody>";
17
+
18
+ for ( const taxon of taxa ) {
19
+ html += "<tr>";
20
+ html += HTML.getElement(
21
+ "td",
22
+ HTML.getLink( "./" + taxon.getFileName(), taxon.getName(), { class: taxon.getHTMLClassName() } ),
23
+ undefined,
24
+ HTML_OPTIONS.NO_ESCAPE
25
+ );
26
+ html += HTML.getElement( "td", taxon.getCommonNames().join( ", " ) );
27
+ html += "</tr>";
28
+ }
29
+
30
+ html += "</tbody>";
31
+ html += "</table>";
32
+
33
+ return html;
34
+ }
35
+
36
+ static getTaxa() {
37
+ return this.#sortedTaxa;
38
+ }
39
+
40
+ static getTaxon( name ) {
41
+ return this.#taxa[ name ];
42
+ }
43
+
44
+ static init( dataDir ) {
45
+ const taxaCSV = CSV.parseFile( dataDir, "taxa.csv" );
46
+ for ( const row of taxaCSV ) {
47
+ const name = row[ "taxon" ];
48
+ const status = row[ "status" ];
49
+ const jepsonID = row[ "jepson id" ];
50
+ switch ( status ) {
51
+ case "N":
52
+ case "NC":
53
+ case "X": {
54
+ if ( this.#taxa[ name ] ) {
55
+ ErrorLog.log( name, "has multiple entries" );
56
+ }
57
+ const commonName = row[ "common name" ];
58
+ this.#taxa[ name ] = new Taxon( name, commonName, status, jepsonID, row[ "calrecnum" ], row[ "inat id" ] );
59
+ if ( !jepsonID ) {
60
+ ErrorLog.log( name, "has no Jepson ID" );
61
+ }
62
+ break;
63
+ }
64
+ default:
65
+ ErrorLog.log( name, "has unrecognized status", status );
66
+ }
67
+ }
68
+
69
+ this.#sortedTaxa = Object.values( this.#taxa ).sort( ( a, b ) => a.getName().localeCompare( b.getName() ) );
70
+
71
+ const synCSV = CSV.parseFile( dataDir, "synonyms.csv" );
72
+ for ( const syn of synCSV ) {
73
+ const currName = syn[ "Current" ];
74
+ const taxon = this.getTaxon( currName );
75
+ if ( !taxon ) {
76
+ ErrorLog.log( currName, "has synonym but not in taxa.csv" );
77
+ continue;
78
+ }
79
+ taxon.addSynonym( syn[ "Former" ], syn[ "Type" ] );
80
+ }
81
+
82
+ }
83
+
84
+ }
85
+
86
+ export { Taxa };
package/lib/taxon.js ADDED
@@ -0,0 +1,116 @@
1
+ import { Config } from "./config.js";
2
+ import { ErrorLog } from "./errorlog.js";
3
+ import { Genera } from "./genera.js";
4
+
5
+ class Taxon {
6
+
7
+ #name;
8
+ #genus;
9
+ #commonNames;
10
+ #status;
11
+ #jepsonID;
12
+ #calRecNum;
13
+ #iNatID;
14
+ #iNatName;
15
+ #synonyms = [];
16
+
17
+ constructor( name, commonNames, status, jepsonID, calRecNum, iNatID ) {
18
+ this.#name = name;
19
+ this.#genus = name.split( " " )[ 0 ];
20
+ this.#commonNames = commonNames ? commonNames.split( "," ).map( t => t.trim() ) : [];
21
+ this.#status = status;
22
+ this.#jepsonID = jepsonID;
23
+ this.#calRecNum = calRecNum;
24
+ this.#iNatID = iNatID;
25
+ Genera.addTaxon( this );
26
+ if ( !calRecNum ) {
27
+ ErrorLog.log( this.getName(), "has no Calflora ID" );
28
+ }
29
+ if ( !iNatID ) {
30
+ ErrorLog.log( this.getName(), "has no iNat ID" );
31
+ }
32
+ }
33
+
34
+ addSynonym( syn, type ) {
35
+ this.#synonyms.push( syn );
36
+ if ( type === "INAT" ) {
37
+ this.#iNatName = syn;
38
+ }
39
+ }
40
+
41
+ getCalfloraName() {
42
+ return this.#name.replace( " subsp.", " ssp." ).replace( "×", "X" );
43
+ }
44
+
45
+ getCalRecNum() {
46
+ return this.#calRecNum;
47
+ }
48
+
49
+ getCommonNames() {
50
+ return this.#commonNames;
51
+ }
52
+
53
+ getFamily() {
54
+ return Genera.getFamily( this.#genus );
55
+ }
56
+
57
+ getFileName( ext = "html" ) {
58
+ // Convert spaces to "-" and remove ".".
59
+ return this.getName().replaceAll( " ", "-" ).replaceAll( ".", "" ) + "." + ext;
60
+ }
61
+
62
+ getGenus() {
63
+ return Genera.getGenus( this.#genus );
64
+ }
65
+
66
+ getHTMLClassName() {
67
+ return this.isNative() ? "native" : "non-native";
68
+ }
69
+
70
+ getGenusName() {
71
+ return this.#genus;
72
+ }
73
+
74
+ getINatID() {
75
+ return this.#iNatID;
76
+ }
77
+
78
+ getINatName() {
79
+ const name = this.#iNatName ? this.#iNatName : this.#name;
80
+ return name.replace( / (subsp|var)\./, "" ).replace( "×", "× " );
81
+ }
82
+
83
+ getJepsonID() {
84
+ return this.#jepsonID;
85
+ }
86
+
87
+ getName() {
88
+ return this.#name;
89
+ }
90
+
91
+ getStatus() {
92
+ return this.#status;
93
+ }
94
+
95
+ getStatusDescription() {
96
+ switch ( this.#status ) {
97
+ case "N":
98
+ return "Native";
99
+ case "NC":
100
+ return Config.getLabel( "status-NC", "Introduced" );
101
+ case "X":
102
+ return "Introduced";
103
+ }
104
+ throw new Error( this.#status );
105
+ }
106
+ getSynonyms() {
107
+ return this.#synonyms;
108
+ }
109
+
110
+ isNative() {
111
+ return this.#status === "N";
112
+ }
113
+
114
+ }
115
+
116
+ export { Taxon };
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@ca-plant-list/ca-plant-list",
3
+ "version": "0.0.0",
4
+ "description": "Tools to create Jekyll files for a website listing plants in an area of California.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ca-plant-list": "build-site.js"
9
+ },
10
+ "dependencies": {
11
+ "csv-parse": "^5.3.1"
12
+ },
13
+ "devDependencies": {
14
+ "eslint": "^8.26.0"
15
+ }
16
+ }