react-cheminfo 0.46.0 → 0.48.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/README.md +69 -0
- package/lib/chart/core/chartDomain.d.ts +14 -0
- package/lib/chart/core/chartDomain.d.ts.map +1 -1
- package/lib/chart/core/chartDomain.js +1 -0
- package/lib/chart/core/chartDomain.js.map +1 -1
- package/lib/chart/core/selectionGeometry.d.ts +4 -2
- package/lib/chart/core/selectionGeometry.d.ts.map +1 -1
- package/lib/chart/core/selectionGeometry.js +4 -2
- package/lib/chart/core/selectionGeometry.js.map +1 -1
- package/lib/chart/core/zoomDomain.d.ts +11 -1
- package/lib/chart/core/zoomDomain.d.ts.map +1 -1
- package/lib/chart/core/zoomDomain.js +25 -5
- package/lib/chart/core/zoomDomain.js.map +1 -1
- package/lib/chart/ui/chartGestures.d.ts +26 -1
- package/lib/chart/ui/chartGestures.d.ts.map +1 -1
- package/lib/chart/ui/chartGestures.js +21 -0
- package/lib/chart/ui/chartGestures.js.map +1 -1
- package/lib/chart/ui/index.d.ts +1 -1
- package/lib/chart/ui/index.d.ts.map +1 -1
- package/lib/chart/ui/index.js +1 -1
- package/lib/chart/ui/index.js.map +1 -1
- package/lib/chart/ui/useChartZoom.d.ts.map +1 -1
- package/lib/chart/ui/useChartZoom.js +17 -1
- package/lib/chart/ui/useChartZoom.js.map +1 -1
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/download/core/cssPaint.d.ts +27 -0
- package/lib/download/core/cssPaint.d.ts.map +1 -0
- package/lib/download/core/cssPaint.js +63 -0
- package/lib/download/core/cssPaint.js.map +1 -0
- package/lib/download/core/drawingMarkup.d.ts +11 -0
- package/lib/download/core/drawingMarkup.d.ts.map +1 -0
- package/lib/download/core/drawingMarkup.js +32 -0
- package/lib/download/core/drawingMarkup.js.map +1 -0
- package/lib/download/core/figureHtml.d.ts +25 -0
- package/lib/download/core/figureHtml.d.ts.map +1 -0
- package/lib/download/core/figureHtml.js +174 -0
- package/lib/download/core/figureHtml.js.map +1 -0
- package/lib/download/core/figureHtmlMarkup.d.ts +107 -0
- package/lib/download/core/figureHtmlMarkup.d.ts.map +1 -0
- package/lib/download/core/figureHtmlMarkup.js +75 -0
- package/lib/download/core/figureHtmlMarkup.js.map +1 -0
- package/lib/download/core/figureSvg.d.ts.map +1 -1
- package/lib/download/core/figureSvg.js +16 -35
- package/lib/download/core/figureSvg.js.map +1 -1
- package/lib/download/core/figureSvgDocument.d.ts +3 -3
- package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
- package/lib/download/core/figureTarget.d.ts +14 -3
- package/lib/download/core/figureTarget.d.ts.map +1 -1
- package/lib/download/core/figureTarget.js +42 -3
- package/lib/download/core/figureTarget.js.map +1 -1
- package/lib/download/core/index.d.ts +1 -1
- package/lib/download/core/index.d.ts.map +1 -1
- package/lib/download/core/index.js +1 -1
- package/lib/download/core/index.js.map +1 -1
- package/lib/download/core/textLines.d.ts +27 -0
- package/lib/download/core/textLines.d.ts.map +1 -0
- package/lib/download/core/textLines.js +51 -0
- package/lib/download/core/textLines.js.map +1 -0
- package/lib/ecosystem/core/lookup.d.ts +2 -1
- package/lib/ecosystem/core/lookup.d.ts.map +1 -1
- package/lib/ecosystem/core/lookup.js +10 -3
- package/lib/ecosystem/core/lookup.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +2 -2
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/ecosystem/core/unlistedSites.d.ts +9 -0
- package/lib/ecosystem/core/unlistedSites.d.ts.map +1 -0
- package/lib/ecosystem/core/unlistedSites.js +35 -0
- package/lib/ecosystem/core/unlistedSites.js.map +1 -0
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +7 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/ir/core/irMode.d.ts.map +1 -1
- package/lib/ir/core/irMode.js +6 -1
- package/lib/ir/core/irMode.js.map +1 -1
- package/lib/locales/de.json +56 -1
- package/lib/locales/en.json +56 -1
- package/lib/locales/es.json +56 -1
- package/lib/locales/fr.json +56 -1
- package/lib/locales/it.json +56 -1
- package/lib/periodic/core/index.d.ts +1 -1
- package/lib/periodic/core/index.d.ts.map +1 -1
- package/lib/periodic/core/layout.d.ts +11 -0
- package/lib/periodic/core/layout.d.ts.map +1 -1
- package/lib/periodic/core/layout.js.map +1 -1
- package/lib/periodic/ui/ElementCell.d.ts +6 -2
- package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
- package/lib/periodic/ui/ElementCell.js +2 -2
- package/lib/periodic/ui/ElementCell.js.map +1 -1
- package/lib/periodic/ui/PeriodicTable.d.ts +3 -98
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +9 -84
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.d.ts +13 -3
- package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.js +10 -20
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
- package/lib/periodic/ui/SelectAllCorner.d.ts +23 -0
- package/lib/periodic/ui/SelectAllCorner.d.ts.map +1 -0
- package/lib/periodic/ui/SelectAllCorner.js +40 -0
- package/lib/periodic/ui/SelectAllCorner.js.map +1 -0
- package/lib/periodic/ui/headerStyles.d.ts +17 -0
- package/lib/periodic/ui/headerStyles.d.ts.map +1 -0
- package/lib/periodic/ui/headerStyles.js +19 -0
- package/lib/periodic/ui/headerStyles.js.map +1 -0
- package/lib/periodic/ui/periodicTableProps.d.ts +115 -0
- package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -0
- package/lib/periodic/ui/periodicTableProps.js +8 -0
- package/lib/periodic/ui/periodicTableProps.js.map +1 -0
- package/lib/periodic/ui/tableLayout.d.ts +26 -0
- package/lib/periodic/ui/tableLayout.d.ts.map +1 -0
- package/lib/periodic/ui/tableLayout.js +86 -0
- package/lib/periodic/ui/tableLayout.js.map +1 -0
- package/lib/split/ui/SplitColumn.d.ts +29 -0
- package/lib/split/ui/SplitColumn.d.ts.map +1 -0
- package/lib/split/ui/SplitColumn.js +19 -0
- package/lib/split/ui/SplitColumn.js.map +1 -0
- package/lib/split/ui/SplitRow.d.ts +14 -53
- package/lib/split/ui/SplitRow.d.ts.map +1 -1
- package/lib/split/ui/SplitRow.js +8 -90
- package/lib/split/ui/SplitRow.js.map +1 -1
- package/lib/split/ui/index.d.ts +3 -0
- package/lib/split/ui/index.d.ts.map +1 -1
- package/lib/split/ui/index.js +1 -0
- package/lib/split/ui/index.js.map +1 -1
- package/lib/split/ui/splitPanes.d.ts +84 -0
- package/lib/split/ui/splitPanes.d.ts.map +1 -0
- package/lib/split/ui/splitPanes.js +125 -0
- package/lib/split/ui/splitPanes.js.map +1 -0
- package/lib/taxonomy/core/index.d.ts +8 -0
- package/lib/taxonomy/core/index.d.ts.map +1 -0
- package/lib/taxonomy/core/index.js +5 -0
- package/lib/taxonomy/core/index.js.map +1 -0
- package/lib/taxonomy/core/lineage.d.ts +83 -0
- package/lib/taxonomy/core/lineage.d.ts.map +1 -0
- package/lib/taxonomy/core/lineage.js +126 -0
- package/lib/taxonomy/core/lineage.js.map +1 -0
- package/lib/taxonomy/core/ncbi.d.ts +8 -0
- package/lib/taxonomy/core/ncbi.d.ts.map +1 -0
- package/lib/taxonomy/core/ncbi.js +10 -0
- package/lib/taxonomy/core/ncbi.js.map +1 -0
- package/lib/taxonomy/core/organismName.d.ts +28 -0
- package/lib/taxonomy/core/organismName.d.ts.map +1 -0
- package/lib/taxonomy/core/organismName.js +188 -0
- package/lib/taxonomy/core/organismName.js.map +1 -0
- package/lib/taxonomy/core/organismWords.d.ts +20 -0
- package/lib/taxonomy/core/organismWords.d.ts.map +1 -0
- package/lib/taxonomy/core/organismWords.js +86 -0
- package/lib/taxonomy/core/organismWords.js.map +1 -0
- package/lib/taxonomy/core/ranks.d.ts +51 -0
- package/lib/taxonomy/core/ranks.d.ts.map +1 -0
- package/lib/taxonomy/core/ranks.js +123 -0
- package/lib/taxonomy/core/ranks.js.map +1 -0
- package/lib/taxonomy/ui/OrganismName.d.ts +28 -0
- package/lib/taxonomy/ui/OrganismName.d.ts.map +1 -0
- package/lib/taxonomy/ui/OrganismName.js +28 -0
- package/lib/taxonomy/ui/OrganismName.js.map +1 -0
- package/lib/taxonomy/ui/TaxonLineage.d.ts +67 -0
- package/lib/taxonomy/ui/TaxonLineage.d.ts.map +1 -0
- package/lib/taxonomy/ui/TaxonLineage.js +45 -0
- package/lib/taxonomy/ui/TaxonLineage.js.map +1 -0
- package/lib/taxonomy/ui/index.d.ts +6 -0
- package/lib/taxonomy/ui/index.d.ts.map +1 -0
- package/lib/taxonomy/ui/index.js +4 -0
- package/lib/taxonomy/ui/index.js.map +1 -0
- package/lib/taxonomy/ui/rankLabel.d.ts +17 -0
- package/lib/taxonomy/ui/rankLabel.d.ts.map +1 -0
- package/lib/taxonomy/ui/rankLabel.js +77 -0
- package/lib/taxonomy/ui/rankLabel.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/chart/core/chartDomain.ts +15 -0
- package/src/chart/core/selectionGeometry.ts +4 -2
- package/src/chart/core/zoomDomain.ts +37 -5
- package/src/chart/ui/chartGestures.ts +41 -1
- package/src/chart/ui/index.ts +1 -1
- package/src/chart/ui/useChartZoom.ts +25 -1
- package/src/core.ts +1 -0
- package/src/download/core/cssPaint.ts +71 -0
- package/src/download/core/drawingMarkup.ts +35 -0
- package/src/download/core/figureHtml.ts +214 -0
- package/src/download/core/figureHtmlMarkup.ts +187 -0
- package/src/download/core/figureSvg.ts +17 -38
- package/src/download/core/figureSvgDocument.ts +3 -3
- package/src/download/core/figureTarget.ts +41 -8
- package/src/download/core/index.ts +1 -0
- package/src/download/core/textLines.ts +67 -0
- package/src/ecosystem/core/lookup.ts +11 -3
- package/src/ecosystem/core/types.ts +4 -2
- package/src/ecosystem/core/unlistedSites.ts +37 -0
- package/src/ecosystem/ui/glyphs.tsx +44 -0
- package/src/ir/core/irMode.ts +6 -1
- package/src/locales/de.json +56 -1
- package/src/locales/en.json +56 -1
- package/src/locales/es.json +56 -1
- package/src/locales/fr.json +56 -1
- package/src/locales/it.json +56 -1
- package/src/periodic/core/index.ts +1 -1
- package/src/periodic/core/layout.ts +12 -0
- package/src/periodic/ui/ElementCell.tsx +8 -4
- package/src/periodic/ui/PeriodicTable.tsx +27 -197
- package/src/periodic/ui/PeriodicTableChrome.tsx +23 -20
- package/src/periodic/ui/SelectAllCorner.tsx +65 -0
- package/src/periodic/ui/headerStyles.ts +23 -0
- package/src/periodic/ui/periodicTableProps.ts +117 -0
- package/src/periodic/ui/tableLayout.ts +96 -0
- package/src/split/ui/SplitColumn.tsx +35 -0
- package/src/split/ui/SplitRow.tsx +16 -208
- package/src/split/ui/index.ts +3 -0
- package/src/split/ui/splitPanes.tsx +283 -0
- package/src/taxonomy/core/index.ts +20 -0
- package/src/taxonomy/core/lineage.ts +161 -0
- package/src/taxonomy/core/ncbi.ts +9 -0
- package/src/taxonomy/core/organismName.ts +217 -0
- package/src/taxonomy/core/organismWords.ts +86 -0
- package/src/taxonomy/core/ranks.ts +134 -0
- package/src/taxonomy/ui/OrganismName.tsx +59 -0
- package/src/taxonomy/ui/TaxonLineage.tsx +183 -0
- package/src/taxonomy/ui/index.ts +5 -0
- package/src/taxonomy/ui/rankLabel.ts +81 -0
- package/src/ui.ts +1 -0
- package/styles/chrome.css +135 -6
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lineage: the taxa from the top of the tree of life down to one of them.
|
|
3
|
+
*
|
|
4
|
+
* It is read three ways, because the family meets it in three shapes: a map of
|
|
5
|
+
* NCBI nodes each pointing at its parent (what a database built on the NCBI
|
|
6
|
+
* dump answers), a record of names by rank (what octochemdb stores), or a list
|
|
7
|
+
* already in order.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
TAXON_RANKS,
|
|
12
|
+
isGenusOrBelow,
|
|
13
|
+
isPrincipalRank,
|
|
14
|
+
rankOrder,
|
|
15
|
+
} from './ranks.ts';
|
|
16
|
+
|
|
17
|
+
/** One taxon of a lineage. */
|
|
18
|
+
export interface Taxon {
|
|
19
|
+
/** The rank, as NCBI writes it: `genus`, `no rank`, `clade`… */
|
|
20
|
+
rank: string;
|
|
21
|
+
/** The scientific name. */
|
|
22
|
+
name: string;
|
|
23
|
+
/**
|
|
24
|
+
* The NCBI Taxonomy id.
|
|
25
|
+
* @default undefined — the taxon is known by its name alone
|
|
26
|
+
*/
|
|
27
|
+
taxId?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The taxa from the top of the tree down to one of them, in that order. */
|
|
31
|
+
export type TaxonLineage = readonly Taxon[];
|
|
32
|
+
|
|
33
|
+
/** A taxon of the NCBI tree, as a map of nodes holds it. */
|
|
34
|
+
export interface TaxonNode {
|
|
35
|
+
/** The scientific name. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** The rank, as NCBI writes it. */
|
|
38
|
+
rank: string;
|
|
39
|
+
/** The id of the taxon above; the root is its own parent. */
|
|
40
|
+
parentId: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The taxa a lineage is summarised by: those of a principal rank (domain,
|
|
45
|
+
* kingdom, phylum, class, order, family, genus, species), and the last one
|
|
46
|
+
* whatever its rank, since it is the taxon the lineage leads to.
|
|
47
|
+
*
|
|
48
|
+
* An NCBI placeholder for an unidentified species ('Streptomyces sp.') says
|
|
49
|
+
* no more than its genus, so it is left out and the genus is the leaf. The
|
|
50
|
+
* root, 'cellular organisms', the clades and the steps of no rank above the
|
|
51
|
+
* leaf never show.
|
|
52
|
+
* @param lineage - The lineage, from the top down.
|
|
53
|
+
* @returns The taxa kept, in the same order.
|
|
54
|
+
*/
|
|
55
|
+
export function principalLineage(lineage: TaxonLineage): Taxon[] {
|
|
56
|
+
const kept: Taxon[] = [];
|
|
57
|
+
let last = lineage.length - 1;
|
|
58
|
+
if (last > 0 && isPlaceholderTaxon(lineage[last] as Taxon)) {
|
|
59
|
+
// The genus becomes the leaf, past 'unclassified Streptomyces'.
|
|
60
|
+
last--;
|
|
61
|
+
while (last > 0 && !isPrincipalRank((lineage[last] as Taxon).rank)) last--;
|
|
62
|
+
}
|
|
63
|
+
for (let index = 0; index <= last; index++) {
|
|
64
|
+
const taxon = lineage[index] as Taxon;
|
|
65
|
+
if (index < last && isPlaceholderTaxon(taxon)) continue;
|
|
66
|
+
if (index === last || isPrincipalRank(taxon.rank)) kept.push(taxon);
|
|
67
|
+
}
|
|
68
|
+
return kept;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a taxon is an NCBI placeholder for an unidentified species of a
|
|
73
|
+
* genus: a species named the genus and 'sp.', such as 'Streptomyces sp.'.
|
|
74
|
+
* One named for a strain ('Streptomyces sp. CNQ-509') is a taxon of its own.
|
|
75
|
+
* @param taxon - The taxon.
|
|
76
|
+
* @returns Whether it is one.
|
|
77
|
+
*/
|
|
78
|
+
export function isPlaceholderTaxon(taxon: Taxon): boolean {
|
|
79
|
+
return taxon.rank === 'species' && /\ssp\.$/.test(taxon.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The lineage of a taxon, walked up a map of NCBI nodes to the root.
|
|
84
|
+
* @param nodes - The nodes, by id: a record keyed by the id written as a
|
|
85
|
+
* string, as JSON delivers it, or a map keyed by the number.
|
|
86
|
+
* @param taxId - The taxon to start from.
|
|
87
|
+
* @returns Every taxon from the root down to it, each with its id; empty when
|
|
88
|
+
* the map does not hold the taxon.
|
|
89
|
+
*/
|
|
90
|
+
export function lineageFromNodes(
|
|
91
|
+
nodes: Readonly<Record<string, TaxonNode>> | ReadonlyMap<number, TaxonNode>,
|
|
92
|
+
taxId: number,
|
|
93
|
+
): Taxon[] {
|
|
94
|
+
const read =
|
|
95
|
+
nodes instanceof Map
|
|
96
|
+
? (id: number) => nodes.get(id)
|
|
97
|
+
: (id: number) =>
|
|
98
|
+
(nodes as Readonly<Record<string, TaxonNode>>)[String(id)];
|
|
99
|
+
const upward: Taxon[] = [];
|
|
100
|
+
const seen = new Set<number>();
|
|
101
|
+
let id = taxId;
|
|
102
|
+
while (!seen.has(id)) {
|
|
103
|
+
const node = read(id);
|
|
104
|
+
if (node === undefined) break;
|
|
105
|
+
seen.add(id);
|
|
106
|
+
upward.push({ rank: node.rank, name: node.name, taxId: id });
|
|
107
|
+
id = node.parentId;
|
|
108
|
+
}
|
|
109
|
+
return upward.toReversed();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The lineage written by a record of names by rank, the shape octochemdb
|
|
114
|
+
* stores (`{ kingdom: 'Fungi', genus: 'Penicillium', … }`).
|
|
115
|
+
*
|
|
116
|
+
* A key is matched whatever its case, so `superKingdom` reads as
|
|
117
|
+
* `superkingdom`; a key that names no rank, and an empty name, are left out.
|
|
118
|
+
* @param ranks - The names, by rank.
|
|
119
|
+
* @returns The taxa, from the top down.
|
|
120
|
+
*/
|
|
121
|
+
export function lineageFromRanks(
|
|
122
|
+
ranks: Readonly<Record<string, string | null | undefined>>,
|
|
123
|
+
): Taxon[] {
|
|
124
|
+
const byRank = new Map<string, string>();
|
|
125
|
+
for (const key of Object.keys(ranks)) {
|
|
126
|
+
const name = ranks[key]?.trim();
|
|
127
|
+
if (name === undefined || name === '') continue;
|
|
128
|
+
const rank = key.toLowerCase();
|
|
129
|
+
if (rankOrder(rank) !== undefined) byRank.set(rank, name);
|
|
130
|
+
}
|
|
131
|
+
const lineage: Taxon[] = [];
|
|
132
|
+
for (const rank of TAXON_RANKS) {
|
|
133
|
+
const name = byRank.get(rank);
|
|
134
|
+
if (name !== undefined) lineage.push({ rank, name });
|
|
135
|
+
}
|
|
136
|
+
return lineage;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which taxa of a lineage sit at the genus or under it — the ones whose names
|
|
141
|
+
* nomenclature sets in italics.
|
|
142
|
+
*
|
|
143
|
+
* A taxon with no rank (a clade, a strain NCBI left unranked) takes the place
|
|
144
|
+
* of the taxa above it: under a genus it is one of the genus's, above it it is
|
|
145
|
+
* a clade like Embryophyta, written upright.
|
|
146
|
+
* @param lineage - The lineage, from the top down.
|
|
147
|
+
* @returns One flag per taxon.
|
|
148
|
+
*/
|
|
149
|
+
export function genusOrBelow(lineage: TaxonLineage): boolean[] {
|
|
150
|
+
const flags: boolean[] = [];
|
|
151
|
+
let below = false;
|
|
152
|
+
for (const taxon of lineage) {
|
|
153
|
+
if (isGenusOrBelow(taxon.rank)) {
|
|
154
|
+
below = true;
|
|
155
|
+
} else if (rankOrder(taxon.rank) !== undefined) {
|
|
156
|
+
below = false;
|
|
157
|
+
}
|
|
158
|
+
flags.push(below);
|
|
159
|
+
}
|
|
160
|
+
return flags;
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page of a taxon in the NCBI Taxonomy Browser.
|
|
3
|
+
* @param taxId - The NCBI Taxonomy id.
|
|
4
|
+
* @returns Its address, e.g.
|
|
5
|
+
* `https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=5076`.
|
|
6
|
+
*/
|
|
7
|
+
export function ncbiTaxonomyUrl(taxId: number): string {
|
|
8
|
+
return `https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=${encodeURIComponent(String(taxId))}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which parts of an organism's name nomenclature sets in italics.
|
|
3
|
+
*
|
|
4
|
+
* The genus, the species epithet and an infraspecific epithet are italic; the
|
|
5
|
+
* authority, a strain, `sp.`, a cultivar and the words announcing a rank
|
|
6
|
+
* (`subsp.`, `var.`, `f.`) are upright. Names arrive the way their source wrote
|
|
7
|
+
* them — "Catharanthus roseus (L.) G. Don", "Streptomyces sp. CHQ-64",
|
|
8
|
+
* "Isodon Shikokiana Var. Occidentalis" — so this reads them by their shape,
|
|
9
|
+
* and a name it cannot place is left upright rather than guessed at.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
BEFORE_EPITHET,
|
|
14
|
+
CANDIDATUS,
|
|
15
|
+
HYBRID,
|
|
16
|
+
NOT_EPITHET,
|
|
17
|
+
NOT_GENUS,
|
|
18
|
+
RANK_MARKERS,
|
|
19
|
+
UNNAMED,
|
|
20
|
+
} from './organismWords.ts';
|
|
21
|
+
|
|
22
|
+
/** A run of an organism's name, set one way. */
|
|
23
|
+
export interface OrganismNamePart {
|
|
24
|
+
/** The text, exactly as the name carries it. */
|
|
25
|
+
text: string;
|
|
26
|
+
/** Whether nomenclature sets it in italics. */
|
|
27
|
+
italic: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Piece {
|
|
31
|
+
text: string;
|
|
32
|
+
word: boolean;
|
|
33
|
+
italic: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CONSORTIUM = /(?<plus>\s+\+\s+)/;
|
|
37
|
+
const VIRUS = /\b(?:virus|viruses|phage|bacteriophage|viroid|satellite)\b/i;
|
|
38
|
+
const SEPARATOR = /^[\s_]+$/;
|
|
39
|
+
const TRAILING_MARK = /^(?<word>.+?)(?<mark>[,;]+)$/;
|
|
40
|
+
const GENUS = /^[A-Z][a-z]+$/;
|
|
41
|
+
const ABBREVIATED_GENUS = /^[A-Z]\.$/;
|
|
42
|
+
// A family, a subfamily or a tribe written where a genus would stand:
|
|
43
|
+
// -aceae, -oideae and -eae in botany, -idae in zoology. No genus ends so.
|
|
44
|
+
const FAMILY = /(?:eae|idae)$/;
|
|
45
|
+
const BRACKETED_GENUS = /^\[(?<genus>[A-Z][a-z]+)\]$/;
|
|
46
|
+
const JOINED_MARKER =
|
|
47
|
+
/^(?<marker>subsp\.|ssp\.|var\.|f\.|pv\.)(?<epithet>[a-z][a-z-]+)$/i;
|
|
48
|
+
const EPITHET = /^[a-zß-öø-ÿ][a-zß-öø-ÿ-]+$/;
|
|
49
|
+
const CAPITALISED_EPITHET = /^[A-Z][a-zß-öø-ÿ-]+$/;
|
|
50
|
+
const STARTS_WITH_LETTER = /^\p{L}/u;
|
|
51
|
+
const STARTS_UPPERCASE = /^\p{Lu}/u;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The runs of an organism's name, each set upright or in italics.
|
|
55
|
+
*
|
|
56
|
+
* Joined together, the runs give back the name exactly. A consortium written
|
|
57
|
+
* `A + B` is read as two names; a virus, and anything that does not open with
|
|
58
|
+
* a genus, is one upright run.
|
|
59
|
+
* @param name - The name, as its source writes it.
|
|
60
|
+
* @returns The runs, consecutive runs never set the same way.
|
|
61
|
+
*/
|
|
62
|
+
export function splitOrganismName(name: string): OrganismNamePart[] {
|
|
63
|
+
if (name === '') return [];
|
|
64
|
+
if (VIRUS.test(name)) return [{ text: name, italic: false }];
|
|
65
|
+
const pieces: Piece[] = [];
|
|
66
|
+
for (const segment of name.split(CONSORTIUM)) {
|
|
67
|
+
if (CONSORTIUM.test(segment)) {
|
|
68
|
+
pieces.push({ text: segment, word: false, italic: false });
|
|
69
|
+
} else {
|
|
70
|
+
pieces.push(...styledPieces(segment));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return mergedRuns(pieces);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function styledPieces(text: string): Piece[] {
|
|
77
|
+
const pieces = tokenize(text);
|
|
78
|
+
const words = pieces.filter((piece) => piece.word);
|
|
79
|
+
styleWords(words);
|
|
80
|
+
for (let index = 0; index < pieces.length; index++) {
|
|
81
|
+
const piece = pieces[index] as Piece;
|
|
82
|
+
if (piece.word) continue;
|
|
83
|
+
piece.italic =
|
|
84
|
+
pieces[index - 1]?.italic === true && pieces[index + 1]?.italic === true;
|
|
85
|
+
}
|
|
86
|
+
return pieces;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function tokenize(text: string): Piece[] {
|
|
90
|
+
const pieces: Piece[] = [];
|
|
91
|
+
for (const chunk of text.split(/(?<gap>[\s_]+)/)) {
|
|
92
|
+
if (chunk === '') continue;
|
|
93
|
+
if (SEPARATOR.test(chunk)) {
|
|
94
|
+
pieces.push({ text: chunk, word: false, italic: false });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// A comma or a semicolon closing a word is punctuation, not part of it.
|
|
98
|
+
const { word: bare = chunk, mark } =
|
|
99
|
+
TRAILING_MARK.exec(chunk)?.groups ?? {};
|
|
100
|
+
const joined = JOINED_MARKER.exec(bare)?.groups;
|
|
101
|
+
const bracketed = BRACKETED_GENUS.exec(bare)?.groups;
|
|
102
|
+
const words =
|
|
103
|
+
joined !== undefined
|
|
104
|
+
? [joined.marker, joined.epithet]
|
|
105
|
+
: bracketed !== undefined
|
|
106
|
+
? ['[', bracketed.genus, ']']
|
|
107
|
+
: [bare];
|
|
108
|
+
for (const word of words) {
|
|
109
|
+
pieces.push({ text: word ?? '', word: true, italic: false });
|
|
110
|
+
}
|
|
111
|
+
if (mark !== undefined) {
|
|
112
|
+
pieces.push({ text: mark, word: false, italic: false });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return pieces;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function styleWords(words: Piece[]): void {
|
|
119
|
+
let index = words[0]?.text === '×' ? 1 : 0;
|
|
120
|
+
const first = words[index];
|
|
121
|
+
if (first === undefined) return;
|
|
122
|
+
if (CANDIDATUS.has(first.text)) {
|
|
123
|
+
first.italic = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let genus: Piece | undefined;
|
|
128
|
+
if (first.text === '[' && words[index + 2]?.text === ']') {
|
|
129
|
+
genus = words[index + 1];
|
|
130
|
+
index += 3;
|
|
131
|
+
} else if (GENUS.test(first.text) || ABBREVIATED_GENUS.test(first.text)) {
|
|
132
|
+
genus = first;
|
|
133
|
+
index += 1;
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
genus === undefined ||
|
|
137
|
+
NOT_GENUS.has(genus.text.toLowerCase()) ||
|
|
138
|
+
FAMILY.test(genus.text)
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
genus.italic = true;
|
|
143
|
+
|
|
144
|
+
const titleCase = isTitleCase(words);
|
|
145
|
+
const next = words[index]?.text;
|
|
146
|
+
if (next === undefined || UNNAMED.has(next.toLowerCase())) return;
|
|
147
|
+
let announced = false;
|
|
148
|
+
if (HYBRID.has(next) || BEFORE_EPITHET.has(next.toLowerCase())) {
|
|
149
|
+
index += 1;
|
|
150
|
+
announced = true;
|
|
151
|
+
}
|
|
152
|
+
if (isEpithet(words[index], titleCase || announced)) {
|
|
153
|
+
(words[index] as Piece).italic = true;
|
|
154
|
+
index += 1;
|
|
155
|
+
// A zoological trinomial names its subspecies with no word announcing it.
|
|
156
|
+
if (isEpithet(words[index], false)) {
|
|
157
|
+
(words[index] as Piece).italic = true;
|
|
158
|
+
index += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (; index < words.length; index++) {
|
|
163
|
+
const word = (words[index] as Piece).text;
|
|
164
|
+
const marks = RANK_MARKERS.has(word.toLowerCase()) || HYBRID.has(word);
|
|
165
|
+
if (marks && isEpithet(words[index + 1], true)) {
|
|
166
|
+
(words[index + 1] as Piece).italic = true;
|
|
167
|
+
index += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isEpithet(word: Piece | undefined, capitalised: boolean): boolean {
|
|
173
|
+
if (word === undefined) return false;
|
|
174
|
+
const lower = word.text.toLowerCase();
|
|
175
|
+
if (
|
|
176
|
+
NOT_EPITHET.has(lower) ||
|
|
177
|
+
UNNAMED.has(lower) ||
|
|
178
|
+
RANK_MARKERS.has(lower) ||
|
|
179
|
+
HYBRID.has(word.text)
|
|
180
|
+
) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
EPITHET.test(word.text) ||
|
|
185
|
+
(capitalised && CAPITALISED_EPITHET.test(word.text))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A source that capitalises every word ("Agave Americana") still means a
|
|
190
|
+
// binomial; the words announcing a rank are left out of the count.
|
|
191
|
+
function isTitleCase(words: readonly Piece[]): boolean {
|
|
192
|
+
let lettered = 0;
|
|
193
|
+
for (const { text } of words) {
|
|
194
|
+
const lower = text.toLowerCase();
|
|
195
|
+
if (RANK_MARKERS.has(lower) || UNNAMED.has(lower) || HYBRID.has(text)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!STARTS_WITH_LETTER.test(text)) continue;
|
|
199
|
+
if (!STARTS_UPPERCASE.test(text)) return false;
|
|
200
|
+
lettered += 1;
|
|
201
|
+
}
|
|
202
|
+
return lettered >= 2;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function mergedRuns(pieces: readonly Piece[]): OrganismNamePart[] {
|
|
206
|
+
const runs: OrganismNamePart[] = [];
|
|
207
|
+
for (const piece of pieces) {
|
|
208
|
+
if (piece.text === '') continue;
|
|
209
|
+
const last = runs.at(-1);
|
|
210
|
+
if (last?.italic === piece.italic) {
|
|
211
|
+
last.text += piece.text;
|
|
212
|
+
} else {
|
|
213
|
+
runs.push({ text: piece.text, italic: piece.italic });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return runs;
|
|
217
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words an organism's name is read by: the signs and abbreviations that
|
|
3
|
+
* announce a rank or qualify a name, and the ordinary words a source writes
|
|
4
|
+
* where a genus or an epithet would stand.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The hybrid sign, and the letters written for it. */
|
|
8
|
+
export const HYBRID = new Set(['×', 'x', 'X']);
|
|
9
|
+
/** The word that opens a provisional bacterial name; it alone is italic. */
|
|
10
|
+
export const CANDIDATUS = new Set(['Candidatus', 'Ca.']);
|
|
11
|
+
/** Qualifiers set before the epithet they qualify, in lower case. */
|
|
12
|
+
export const BEFORE_EPITHET = new Set(['cf.', 'cf', 'aff.', 'aff', 'nr.']);
|
|
13
|
+
/** An unnamed species of the genus, in lower case. */
|
|
14
|
+
export const UNNAMED = new Set(['sp.', 'sp', 'spp.', 'spp']);
|
|
15
|
+
/** The abbreviations announcing a lower-rank epithet, in lower case. */
|
|
16
|
+
export const RANK_MARKERS = new Set([
|
|
17
|
+
'subsp.',
|
|
18
|
+
'ssp.',
|
|
19
|
+
'nothosubsp.',
|
|
20
|
+
'var.',
|
|
21
|
+
'nothovar.',
|
|
22
|
+
'subvar.',
|
|
23
|
+
'f.',
|
|
24
|
+
'fo.',
|
|
25
|
+
'forma',
|
|
26
|
+
'subf.',
|
|
27
|
+
'pv.',
|
|
28
|
+
'subgen.',
|
|
29
|
+
'sect.',
|
|
30
|
+
'subsect.',
|
|
31
|
+
'ser.',
|
|
32
|
+
'subser.',
|
|
33
|
+
]);
|
|
34
|
+
/** Words that open a description rather than a binomial, in lower case. */
|
|
35
|
+
export const NOT_GENUS = new Set([
|
|
36
|
+
'unknown',
|
|
37
|
+
'unidentified',
|
|
38
|
+
'uncultured',
|
|
39
|
+
'unclassified',
|
|
40
|
+
'environmental',
|
|
41
|
+
'fungal',
|
|
42
|
+
'bacterial',
|
|
43
|
+
'marine',
|
|
44
|
+
'endophytic',
|
|
45
|
+
'mixed',
|
|
46
|
+
'fungus',
|
|
47
|
+
'bacterium',
|
|
48
|
+
'endophyte',
|
|
49
|
+
'strain',
|
|
50
|
+
'isolate',
|
|
51
|
+
'plant',
|
|
52
|
+
'soil',
|
|
53
|
+
'sponge',
|
|
54
|
+
]);
|
|
55
|
+
/** Lower-case words that follow a binomial without being an epithet. */
|
|
56
|
+
export const NOT_EPITHET = new Set([
|
|
57
|
+
'strain',
|
|
58
|
+
'isolate',
|
|
59
|
+
'symbiont',
|
|
60
|
+
'endophyte',
|
|
61
|
+
'clone',
|
|
62
|
+
'group',
|
|
63
|
+
'complex',
|
|
64
|
+
'culture',
|
|
65
|
+
'cultivar',
|
|
66
|
+
'serovar',
|
|
67
|
+
'biovar',
|
|
68
|
+
'pathovar',
|
|
69
|
+
'species',
|
|
70
|
+
'incertae',
|
|
71
|
+
'sedis',
|
|
72
|
+
'and',
|
|
73
|
+
'et',
|
|
74
|
+
'ex',
|
|
75
|
+
'in',
|
|
76
|
+
'non',
|
|
77
|
+
'sensu',
|
|
78
|
+
'auct',
|
|
79
|
+
'de',
|
|
80
|
+
'du',
|
|
81
|
+
'la',
|
|
82
|
+
'le',
|
|
83
|
+
'van',
|
|
84
|
+
'von',
|
|
85
|
+
'der',
|
|
86
|
+
]);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ranks of the NCBI Taxonomy, from the top of the tree down.
|
|
3
|
+
*
|
|
4
|
+
* NCBI also files taxa under ranks that are not steps of a hierarchy — `no
|
|
5
|
+
* rank`, `clade`, the two roots — and those are deliberately absent here: a
|
|
6
|
+
* clade can sit anywhere, so it has no place in an order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every rank NCBI places in its hierarchy, from the top down.
|
|
11
|
+
*
|
|
12
|
+
* `realm` heads the viruses and `domain` the cellular organisms, the name NCBI
|
|
13
|
+
* gave in 2025 to what it called `superkingdom` before; all three stand at the
|
|
14
|
+
* top, so a lineage read from an older dump sorts the same way.
|
|
15
|
+
*/
|
|
16
|
+
export const TAXON_RANKS = [
|
|
17
|
+
'realm',
|
|
18
|
+
'domain',
|
|
19
|
+
'superkingdom',
|
|
20
|
+
'kingdom',
|
|
21
|
+
'subkingdom',
|
|
22
|
+
'superphylum',
|
|
23
|
+
'phylum',
|
|
24
|
+
'subphylum',
|
|
25
|
+
'superclass',
|
|
26
|
+
'class',
|
|
27
|
+
'subclass',
|
|
28
|
+
'infraclass',
|
|
29
|
+
'cohort',
|
|
30
|
+
'subcohort',
|
|
31
|
+
'superorder',
|
|
32
|
+
'order',
|
|
33
|
+
'suborder',
|
|
34
|
+
'infraorder',
|
|
35
|
+
'parvorder',
|
|
36
|
+
'superfamily',
|
|
37
|
+
'family',
|
|
38
|
+
'subfamily',
|
|
39
|
+
'tribe',
|
|
40
|
+
'subtribe',
|
|
41
|
+
'genus',
|
|
42
|
+
'subgenus',
|
|
43
|
+
'section',
|
|
44
|
+
'subsection',
|
|
45
|
+
'series',
|
|
46
|
+
'subseries',
|
|
47
|
+
'species group',
|
|
48
|
+
'species subgroup',
|
|
49
|
+
'species',
|
|
50
|
+
'forma specialis',
|
|
51
|
+
'subspecies',
|
|
52
|
+
'varietas',
|
|
53
|
+
'subvariety',
|
|
54
|
+
'forma',
|
|
55
|
+
'pathogroup',
|
|
56
|
+
'serogroup',
|
|
57
|
+
'serotype',
|
|
58
|
+
'biotype',
|
|
59
|
+
'genotype',
|
|
60
|
+
'morph',
|
|
61
|
+
'strain',
|
|
62
|
+
'isolate',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/** A rank of the NCBI hierarchy. */
|
|
66
|
+
export type TaxonRank = (typeof TAXON_RANKS)[number];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The ranks a lineage is summarised by: the eight a textbook names, with the
|
|
70
|
+
* three names the top of the tree goes by.
|
|
71
|
+
*/
|
|
72
|
+
export const PRINCIPAL_RANKS = [
|
|
73
|
+
'realm',
|
|
74
|
+
'domain',
|
|
75
|
+
'superkingdom',
|
|
76
|
+
'kingdom',
|
|
77
|
+
'phylum',
|
|
78
|
+
'class',
|
|
79
|
+
'order',
|
|
80
|
+
'family',
|
|
81
|
+
'genus',
|
|
82
|
+
'species',
|
|
83
|
+
] as const satisfies readonly TaxonRank[];
|
|
84
|
+
|
|
85
|
+
const ORDER = new Map<string, number>();
|
|
86
|
+
for (let index = 0; index < TAXON_RANKS.length; index++) {
|
|
87
|
+
ORDER.set(TAXON_RANKS[index] as string, index);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const PRINCIPAL = new Set<string>(PRINCIPAL_RANKS);
|
|
91
|
+
|
|
92
|
+
const GENUS = ORDER.get('genus') ?? 0;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Where a rank stands in the hierarchy.
|
|
96
|
+
* @param rank - The rank, as NCBI writes it.
|
|
97
|
+
* @returns Its position from the top, 0 for `realm`, or `undefined` for a
|
|
98
|
+
* rank that is not a step of the hierarchy (`no rank`, `clade`).
|
|
99
|
+
*/
|
|
100
|
+
export function rankOrder(rank: string): number | undefined {
|
|
101
|
+
return ORDER.get(rank);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether a rank is one a lineage is summarised by.
|
|
106
|
+
* @param rank - The rank, as NCBI writes it.
|
|
107
|
+
* @returns Whether it is one of {@link PRINCIPAL_RANKS}.
|
|
108
|
+
*/
|
|
109
|
+
export function isPrincipalRank(rank: string): boolean {
|
|
110
|
+
return PRINCIPAL.has(rank);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a rank is the genus or one under it — the ranks whose names
|
|
115
|
+
* nomenclature sets in italics.
|
|
116
|
+
* @param rank - The rank, as NCBI writes it.
|
|
117
|
+
* @returns `false` for a rank above the genus and for one with no place in the
|
|
118
|
+
* hierarchy.
|
|
119
|
+
*/
|
|
120
|
+
export function isGenusOrBelow(rank: string): boolean {
|
|
121
|
+
const order = ORDER.get(rank);
|
|
122
|
+
return order !== undefined && order >= GENUS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether a rank stands above the genus, so its name is set upright.
|
|
127
|
+
* @param rank - The rank, as NCBI writes it.
|
|
128
|
+
* @returns `false` for the genus and below, and for a rank with no place in
|
|
129
|
+
* the hierarchy.
|
|
130
|
+
*/
|
|
131
|
+
export function isAboveGenus(rank: string): boolean {
|
|
132
|
+
const order = ORDER.get(rank);
|
|
133
|
+
return order !== undefined && order < GENUS;
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
|
|
4
|
+
import { joinClassNames } from '../../shared/ui/joinClassNames.ts';
|
|
5
|
+
import { splitOrganismName } from '../core/organismName.ts';
|
|
6
|
+
import { isAboveGenus } from '../core/ranks.ts';
|
|
7
|
+
|
|
8
|
+
/** What an {@link OrganismName} needs. */
|
|
9
|
+
export interface OrganismNameProps {
|
|
10
|
+
/** The name, as its source writes it. */
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* The rank of the taxon the name belongs to. A name ranked above the genus
|
|
14
|
+
* (a family, an order) is set upright whatever its shape.
|
|
15
|
+
* @default undefined — the name is read by its shape alone
|
|
16
|
+
*/
|
|
17
|
+
rank?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Class names added to the root element.
|
|
20
|
+
* @default undefined
|
|
21
|
+
*/
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An organism's name, with the genus and the epithets in italics and the
|
|
27
|
+
* authority, the strain and `sp.` upright, as nomenclature sets them.
|
|
28
|
+
*
|
|
29
|
+
* The italics are `<i>`, the element HTML names for a taxonomic designation,
|
|
30
|
+
* and the name keeps every character its source gave it.
|
|
31
|
+
* @param props - See {@link OrganismNameProps}.
|
|
32
|
+
* @returns The name.
|
|
33
|
+
*/
|
|
34
|
+
export function OrganismName(props: OrganismNameProps): ReactElement {
|
|
35
|
+
const { name, rank, className } = props;
|
|
36
|
+
const parts =
|
|
37
|
+
rank !== undefined && isAboveGenus(rank)
|
|
38
|
+
? [{ text: name, italic: false }]
|
|
39
|
+
: splitOrganismName(name);
|
|
40
|
+
|
|
41
|
+
const children: ReactNode[] = [];
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
children.push(
|
|
45
|
+
part.italic ? (
|
|
46
|
+
<i key={offset}>{part.text}</i>
|
|
47
|
+
) : (
|
|
48
|
+
<Fragment key={offset}>{part.text}</Fragment>
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
offset += part.text.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<span className={joinClassNames('organism-name', className)}>
|
|
56
|
+
{children}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|