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.
Files changed (229) hide show
  1. package/README.md +69 -0
  2. package/lib/chart/core/chartDomain.d.ts +14 -0
  3. package/lib/chart/core/chartDomain.d.ts.map +1 -1
  4. package/lib/chart/core/chartDomain.js +1 -0
  5. package/lib/chart/core/chartDomain.js.map +1 -1
  6. package/lib/chart/core/selectionGeometry.d.ts +4 -2
  7. package/lib/chart/core/selectionGeometry.d.ts.map +1 -1
  8. package/lib/chart/core/selectionGeometry.js +4 -2
  9. package/lib/chart/core/selectionGeometry.js.map +1 -1
  10. package/lib/chart/core/zoomDomain.d.ts +11 -1
  11. package/lib/chart/core/zoomDomain.d.ts.map +1 -1
  12. package/lib/chart/core/zoomDomain.js +25 -5
  13. package/lib/chart/core/zoomDomain.js.map +1 -1
  14. package/lib/chart/ui/chartGestures.d.ts +26 -1
  15. package/lib/chart/ui/chartGestures.d.ts.map +1 -1
  16. package/lib/chart/ui/chartGestures.js +21 -0
  17. package/lib/chart/ui/chartGestures.js.map +1 -1
  18. package/lib/chart/ui/index.d.ts +1 -1
  19. package/lib/chart/ui/index.d.ts.map +1 -1
  20. package/lib/chart/ui/index.js +1 -1
  21. package/lib/chart/ui/index.js.map +1 -1
  22. package/lib/chart/ui/useChartZoom.d.ts.map +1 -1
  23. package/lib/chart/ui/useChartZoom.js +17 -1
  24. package/lib/chart/ui/useChartZoom.js.map +1 -1
  25. package/lib/core.d.ts +1 -0
  26. package/lib/core.d.ts.map +1 -1
  27. package/lib/core.js +1 -0
  28. package/lib/core.js.map +1 -1
  29. package/lib/download/core/cssPaint.d.ts +27 -0
  30. package/lib/download/core/cssPaint.d.ts.map +1 -0
  31. package/lib/download/core/cssPaint.js +63 -0
  32. package/lib/download/core/cssPaint.js.map +1 -0
  33. package/lib/download/core/drawingMarkup.d.ts +11 -0
  34. package/lib/download/core/drawingMarkup.d.ts.map +1 -0
  35. package/lib/download/core/drawingMarkup.js +32 -0
  36. package/lib/download/core/drawingMarkup.js.map +1 -0
  37. package/lib/download/core/figureHtml.d.ts +25 -0
  38. package/lib/download/core/figureHtml.d.ts.map +1 -0
  39. package/lib/download/core/figureHtml.js +174 -0
  40. package/lib/download/core/figureHtml.js.map +1 -0
  41. package/lib/download/core/figureHtmlMarkup.d.ts +107 -0
  42. package/lib/download/core/figureHtmlMarkup.d.ts.map +1 -0
  43. package/lib/download/core/figureHtmlMarkup.js +75 -0
  44. package/lib/download/core/figureHtmlMarkup.js.map +1 -0
  45. package/lib/download/core/figureSvg.d.ts.map +1 -1
  46. package/lib/download/core/figureSvg.js +16 -35
  47. package/lib/download/core/figureSvg.js.map +1 -1
  48. package/lib/download/core/figureSvgDocument.d.ts +3 -3
  49. package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
  50. package/lib/download/core/figureTarget.d.ts +14 -3
  51. package/lib/download/core/figureTarget.d.ts.map +1 -1
  52. package/lib/download/core/figureTarget.js +42 -3
  53. package/lib/download/core/figureTarget.js.map +1 -1
  54. package/lib/download/core/index.d.ts +1 -1
  55. package/lib/download/core/index.d.ts.map +1 -1
  56. package/lib/download/core/index.js +1 -1
  57. package/lib/download/core/index.js.map +1 -1
  58. package/lib/download/core/textLines.d.ts +27 -0
  59. package/lib/download/core/textLines.d.ts.map +1 -0
  60. package/lib/download/core/textLines.js +51 -0
  61. package/lib/download/core/textLines.js.map +1 -0
  62. package/lib/ecosystem/core/lookup.d.ts +2 -1
  63. package/lib/ecosystem/core/lookup.d.ts.map +1 -1
  64. package/lib/ecosystem/core/lookup.js +10 -3
  65. package/lib/ecosystem/core/lookup.js.map +1 -1
  66. package/lib/ecosystem/core/types.d.ts +2 -2
  67. package/lib/ecosystem/core/types.d.ts.map +1 -1
  68. package/lib/ecosystem/core/unlistedSites.d.ts +9 -0
  69. package/lib/ecosystem/core/unlistedSites.d.ts.map +1 -0
  70. package/lib/ecosystem/core/unlistedSites.js +35 -0
  71. package/lib/ecosystem/core/unlistedSites.js.map +1 -0
  72. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  73. package/lib/ecosystem/ui/glyphs.js +7 -0
  74. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  75. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  76. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  77. package/lib/ir/core/irMode.d.ts.map +1 -1
  78. package/lib/ir/core/irMode.js +6 -1
  79. package/lib/ir/core/irMode.js.map +1 -1
  80. package/lib/locales/de.json +56 -1
  81. package/lib/locales/en.json +56 -1
  82. package/lib/locales/es.json +56 -1
  83. package/lib/locales/fr.json +56 -1
  84. package/lib/locales/it.json +56 -1
  85. package/lib/periodic/core/index.d.ts +1 -1
  86. package/lib/periodic/core/index.d.ts.map +1 -1
  87. package/lib/periodic/core/layout.d.ts +11 -0
  88. package/lib/periodic/core/layout.d.ts.map +1 -1
  89. package/lib/periodic/core/layout.js.map +1 -1
  90. package/lib/periodic/ui/ElementCell.d.ts +6 -2
  91. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  92. package/lib/periodic/ui/ElementCell.js +2 -2
  93. package/lib/periodic/ui/ElementCell.js.map +1 -1
  94. package/lib/periodic/ui/PeriodicTable.d.ts +3 -98
  95. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  96. package/lib/periodic/ui/PeriodicTable.js +9 -84
  97. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  98. package/lib/periodic/ui/PeriodicTableChrome.d.ts +13 -3
  99. package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
  100. package/lib/periodic/ui/PeriodicTableChrome.js +10 -20
  101. package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
  102. package/lib/periodic/ui/SelectAllCorner.d.ts +23 -0
  103. package/lib/periodic/ui/SelectAllCorner.d.ts.map +1 -0
  104. package/lib/periodic/ui/SelectAllCorner.js +40 -0
  105. package/lib/periodic/ui/SelectAllCorner.js.map +1 -0
  106. package/lib/periodic/ui/headerStyles.d.ts +17 -0
  107. package/lib/periodic/ui/headerStyles.d.ts.map +1 -0
  108. package/lib/periodic/ui/headerStyles.js +19 -0
  109. package/lib/periodic/ui/headerStyles.js.map +1 -0
  110. package/lib/periodic/ui/periodicTableProps.d.ts +115 -0
  111. package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -0
  112. package/lib/periodic/ui/periodicTableProps.js +8 -0
  113. package/lib/periodic/ui/periodicTableProps.js.map +1 -0
  114. package/lib/periodic/ui/tableLayout.d.ts +26 -0
  115. package/lib/periodic/ui/tableLayout.d.ts.map +1 -0
  116. package/lib/periodic/ui/tableLayout.js +86 -0
  117. package/lib/periodic/ui/tableLayout.js.map +1 -0
  118. package/lib/split/ui/SplitColumn.d.ts +29 -0
  119. package/lib/split/ui/SplitColumn.d.ts.map +1 -0
  120. package/lib/split/ui/SplitColumn.js +19 -0
  121. package/lib/split/ui/SplitColumn.js.map +1 -0
  122. package/lib/split/ui/SplitRow.d.ts +14 -53
  123. package/lib/split/ui/SplitRow.d.ts.map +1 -1
  124. package/lib/split/ui/SplitRow.js +8 -90
  125. package/lib/split/ui/SplitRow.js.map +1 -1
  126. package/lib/split/ui/index.d.ts +3 -0
  127. package/lib/split/ui/index.d.ts.map +1 -1
  128. package/lib/split/ui/index.js +1 -0
  129. package/lib/split/ui/index.js.map +1 -1
  130. package/lib/split/ui/splitPanes.d.ts +84 -0
  131. package/lib/split/ui/splitPanes.d.ts.map +1 -0
  132. package/lib/split/ui/splitPanes.js +125 -0
  133. package/lib/split/ui/splitPanes.js.map +1 -0
  134. package/lib/taxonomy/core/index.d.ts +8 -0
  135. package/lib/taxonomy/core/index.d.ts.map +1 -0
  136. package/lib/taxonomy/core/index.js +5 -0
  137. package/lib/taxonomy/core/index.js.map +1 -0
  138. package/lib/taxonomy/core/lineage.d.ts +83 -0
  139. package/lib/taxonomy/core/lineage.d.ts.map +1 -0
  140. package/lib/taxonomy/core/lineage.js +126 -0
  141. package/lib/taxonomy/core/lineage.js.map +1 -0
  142. package/lib/taxonomy/core/ncbi.d.ts +8 -0
  143. package/lib/taxonomy/core/ncbi.d.ts.map +1 -0
  144. package/lib/taxonomy/core/ncbi.js +10 -0
  145. package/lib/taxonomy/core/ncbi.js.map +1 -0
  146. package/lib/taxonomy/core/organismName.d.ts +28 -0
  147. package/lib/taxonomy/core/organismName.d.ts.map +1 -0
  148. package/lib/taxonomy/core/organismName.js +188 -0
  149. package/lib/taxonomy/core/organismName.js.map +1 -0
  150. package/lib/taxonomy/core/organismWords.d.ts +20 -0
  151. package/lib/taxonomy/core/organismWords.d.ts.map +1 -0
  152. package/lib/taxonomy/core/organismWords.js +86 -0
  153. package/lib/taxonomy/core/organismWords.js.map +1 -0
  154. package/lib/taxonomy/core/ranks.d.ts +51 -0
  155. package/lib/taxonomy/core/ranks.d.ts.map +1 -0
  156. package/lib/taxonomy/core/ranks.js +123 -0
  157. package/lib/taxonomy/core/ranks.js.map +1 -0
  158. package/lib/taxonomy/ui/OrganismName.d.ts +28 -0
  159. package/lib/taxonomy/ui/OrganismName.d.ts.map +1 -0
  160. package/lib/taxonomy/ui/OrganismName.js +28 -0
  161. package/lib/taxonomy/ui/OrganismName.js.map +1 -0
  162. package/lib/taxonomy/ui/TaxonLineage.d.ts +67 -0
  163. package/lib/taxonomy/ui/TaxonLineage.d.ts.map +1 -0
  164. package/lib/taxonomy/ui/TaxonLineage.js +45 -0
  165. package/lib/taxonomy/ui/TaxonLineage.js.map +1 -0
  166. package/lib/taxonomy/ui/index.d.ts +6 -0
  167. package/lib/taxonomy/ui/index.d.ts.map +1 -0
  168. package/lib/taxonomy/ui/index.js +4 -0
  169. package/lib/taxonomy/ui/index.js.map +1 -0
  170. package/lib/taxonomy/ui/rankLabel.d.ts +17 -0
  171. package/lib/taxonomy/ui/rankLabel.d.ts.map +1 -0
  172. package/lib/taxonomy/ui/rankLabel.js +77 -0
  173. package/lib/taxonomy/ui/rankLabel.js.map +1 -0
  174. package/lib/ui.d.ts +1 -0
  175. package/lib/ui.d.ts.map +1 -1
  176. package/lib/ui.js +1 -0
  177. package/lib/ui.js.map +1 -1
  178. package/package.json +1 -1
  179. package/src/chart/core/chartDomain.ts +15 -0
  180. package/src/chart/core/selectionGeometry.ts +4 -2
  181. package/src/chart/core/zoomDomain.ts +37 -5
  182. package/src/chart/ui/chartGestures.ts +41 -1
  183. package/src/chart/ui/index.ts +1 -1
  184. package/src/chart/ui/useChartZoom.ts +25 -1
  185. package/src/core.ts +1 -0
  186. package/src/download/core/cssPaint.ts +71 -0
  187. package/src/download/core/drawingMarkup.ts +35 -0
  188. package/src/download/core/figureHtml.ts +214 -0
  189. package/src/download/core/figureHtmlMarkup.ts +187 -0
  190. package/src/download/core/figureSvg.ts +17 -38
  191. package/src/download/core/figureSvgDocument.ts +3 -3
  192. package/src/download/core/figureTarget.ts +41 -8
  193. package/src/download/core/index.ts +1 -0
  194. package/src/download/core/textLines.ts +67 -0
  195. package/src/ecosystem/core/lookup.ts +11 -3
  196. package/src/ecosystem/core/types.ts +4 -2
  197. package/src/ecosystem/core/unlistedSites.ts +37 -0
  198. package/src/ecosystem/ui/glyphs.tsx +44 -0
  199. package/src/ir/core/irMode.ts +6 -1
  200. package/src/locales/de.json +56 -1
  201. package/src/locales/en.json +56 -1
  202. package/src/locales/es.json +56 -1
  203. package/src/locales/fr.json +56 -1
  204. package/src/locales/it.json +56 -1
  205. package/src/periodic/core/index.ts +1 -1
  206. package/src/periodic/core/layout.ts +12 -0
  207. package/src/periodic/ui/ElementCell.tsx +8 -4
  208. package/src/periodic/ui/PeriodicTable.tsx +27 -197
  209. package/src/periodic/ui/PeriodicTableChrome.tsx +23 -20
  210. package/src/periodic/ui/SelectAllCorner.tsx +65 -0
  211. package/src/periodic/ui/headerStyles.ts +23 -0
  212. package/src/periodic/ui/periodicTableProps.ts +117 -0
  213. package/src/periodic/ui/tableLayout.ts +96 -0
  214. package/src/split/ui/SplitColumn.tsx +35 -0
  215. package/src/split/ui/SplitRow.tsx +16 -208
  216. package/src/split/ui/index.ts +3 -0
  217. package/src/split/ui/splitPanes.tsx +283 -0
  218. package/src/taxonomy/core/index.ts +20 -0
  219. package/src/taxonomy/core/lineage.ts +161 -0
  220. package/src/taxonomy/core/ncbi.ts +9 -0
  221. package/src/taxonomy/core/organismName.ts +217 -0
  222. package/src/taxonomy/core/organismWords.ts +86 -0
  223. package/src/taxonomy/core/ranks.ts +134 -0
  224. package/src/taxonomy/ui/OrganismName.tsx +59 -0
  225. package/src/taxonomy/ui/TaxonLineage.tsx +183 -0
  226. package/src/taxonomy/ui/index.ts +5 -0
  227. package/src/taxonomy/ui/rankLabel.ts +81 -0
  228. package/src/ui.ts +1 -0
  229. package/styles/chrome.css +135 -6
@@ -0,0 +1,187 @@
1
+ /**
2
+ * A part of a figure drawn in HTML, written out as SVG.
3
+ *
4
+ * The part is measured off the page first (`figureHtml.ts`); what reaches this
5
+ * module is already a list of boxes, runs of words and drawings with where each
6
+ * one sits, so the file it becomes can be read — and tested — without a page.
7
+ */
8
+
9
+ /** A box the browser painted: a ground, a line round it, or both. */
10
+ export interface HtmlPaintBox {
11
+ /** What it is. */
12
+ kind: 'box';
13
+ /** Its left edge, in pixels from the left of the part. */
14
+ x: number;
15
+ /** Its top edge, in pixels from the top of the part. */
16
+ y: number;
17
+ /** Its width, in pixels. */
18
+ width: number;
19
+ /** Its height. */
20
+ height: number;
21
+ /**
22
+ * How round its corners are, in pixels.
23
+ * @default 0
24
+ */
25
+ radius?: number;
26
+ /**
27
+ * What fills it.
28
+ * @default undefined — it is not filled
29
+ */
30
+ fill?: string;
31
+ /**
32
+ * The colour of the line round it, centred on its edge.
33
+ * @default undefined — no line is drawn
34
+ */
35
+ stroke?: string;
36
+ /**
37
+ * How heavy that line is, in pixels.
38
+ * @default 1
39
+ */
40
+ strokeWidth?: number;
41
+ /**
42
+ * The line's dashes and gaps, in pixels.
43
+ * @default undefined — a solid line
44
+ */
45
+ dash?: readonly number[];
46
+ }
47
+
48
+ /** One line of words, set where the browser set it. */
49
+ export interface HtmlPaintText {
50
+ /** What it is. */
51
+ kind: 'text';
52
+ /** What it says, its spaces already collapsed. */
53
+ text: string;
54
+ /** Its left edge, in pixels from the left of the part. */
55
+ x: number;
56
+ /** The middle of its line, in pixels from the top of the part. */
57
+ y: number;
58
+ /**
59
+ * How wide the browser drew it. The words are fitted to it, so a run the
60
+ * page condensed stays condensed and a reader opening the file with another
61
+ * font gets the same layout.
62
+ */
63
+ width: number;
64
+ /** Its colour. */
65
+ color: string;
66
+ /** Its size, in pixels. */
67
+ fontSize: number;
68
+ /** Its weight. */
69
+ fontWeight: string;
70
+ /**
71
+ * Its style.
72
+ * @default undefined — upright
73
+ */
74
+ fontStyle?: string;
75
+ /**
76
+ * Its type, where it is not the one the document is set in.
77
+ * @default undefined — the document's own
78
+ */
79
+ fontFamily?: string;
80
+ }
81
+
82
+ /** A drawing inside the part, already an SVG of its own. */
83
+ export interface HtmlPaintDrawing {
84
+ /** What it is. */
85
+ kind: 'drawing';
86
+ /** The `<svg>` element as it was serialized, tokens already resolved. */
87
+ markup: string;
88
+ /** Its left edge, in pixels from the left of the part. */
89
+ x: number;
90
+ /** Its top edge, in pixels from the top of the part. */
91
+ y: number;
92
+ }
93
+
94
+ /** Everything inside an element the page drew faded. */
95
+ export interface HtmlPaintGroup {
96
+ /** What it is. */
97
+ kind: 'group';
98
+ /** How opaque the whole of it is, from 0 to 1. */
99
+ opacity: number;
100
+ /** What it holds, in the order it is painted. */
101
+ children: readonly HtmlPaint[];
102
+ }
103
+
104
+ /** One thing painted from a part of the page. */
105
+ export type HtmlPaint =
106
+ HtmlPaintBox | HtmlPaintText | HtmlPaintDrawing | HtmlPaintGroup;
107
+
108
+ /**
109
+ * The part, as the markup that goes into the saved figure.
110
+ * @param paints - What was measured, in the order it is painted.
111
+ * @returns The markup, in the part's own coordinates: the caller places it.
112
+ */
113
+ export function figureHtmlMarkup(paints: readonly HtmlPaint[]): string {
114
+ let markup = '';
115
+ for (const paint of paints) markup += paintMarkup(paint);
116
+ return markup;
117
+ }
118
+
119
+ function paintMarkup(paint: HtmlPaint): string {
120
+ switch (paint.kind) {
121
+ case 'box':
122
+ return boxMarkup(paint);
123
+ case 'text':
124
+ return textMarkup(paint);
125
+ case 'drawing':
126
+ return `<g transform="translate(${round(paint.x)} ${round(paint.y)})">${paint.markup}</g>`;
127
+ case 'group':
128
+ return `<g opacity="${round(paint.opacity)}">${figureHtmlMarkup(paint.children)}</g>`;
129
+ default:
130
+ return '';
131
+ }
132
+ }
133
+
134
+ function boxMarkup(box: HtmlPaintBox): string {
135
+ const { x, y, width, height, fill, stroke, dash } = box;
136
+ const { radius = 0, strokeWidth = 1 } = box;
137
+ if (fill === undefined && stroke === undefined) return '';
138
+ const line =
139
+ stroke === undefined
140
+ ? ''
141
+ : ` stroke="${attribute(stroke)}" stroke-width="${round(strokeWidth)}"${
142
+ dash === undefined || dash.length === 0
143
+ ? ''
144
+ : ` stroke-dasharray="${dash.map(round).join(' ')}"`
145
+ }`;
146
+ return (
147
+ `<rect x="${round(x)}" y="${round(y)}"` +
148
+ ` width="${round(Math.max(0, width))}" height="${round(Math.max(0, height))}"${
149
+ radius > 0 ? ` rx="${round(radius)}"` : ''
150
+ } fill="${attribute(fill ?? 'none')}"${line}/>`
151
+ );
152
+ }
153
+
154
+ function textMarkup(run: HtmlPaintText): string {
155
+ if (run.text === '') return '';
156
+ const style =
157
+ run.fontStyle === undefined || run.fontStyle === 'normal'
158
+ ? ''
159
+ : ` font-style="${attribute(run.fontStyle)}"`;
160
+ const family =
161
+ run.fontFamily === undefined
162
+ ? ''
163
+ : ` font-family="${attribute(run.fontFamily)}"`;
164
+ return (
165
+ `<text x="${round(run.x)}" y="${round(run.y)}" dominant-baseline="central"` +
166
+ ` textLength="${round(run.width)}" lengthAdjust="spacingAndGlyphs"` +
167
+ ` fill="${attribute(run.color)}" font-size="${round(run.fontSize)}px"` +
168
+ ` font-weight="${attribute(run.fontWeight)}"${style}${family}>` +
169
+ `${text(run.text)}</text>`
170
+ );
171
+ }
172
+
173
+ function attribute(value: string): string {
174
+ return text(value).replaceAll('"', '&quot;');
175
+ }
176
+
177
+ function text(value: string): string {
178
+ return value
179
+ .replaceAll('&', '&amp;')
180
+ .replaceAll('<', '&lt;')
181
+ .replaceAll('>', '&gt;');
182
+ }
183
+
184
+ function round(value: number): number {
185
+ if (!Number.isFinite(value)) return 0;
186
+ return Math.round(value * 100) / 100;
187
+ }
@@ -7,7 +7,8 @@
7
7
  * around it and would otherwise be reset to whatever opens the file.
8
8
  */
9
9
 
10
- import { cssTokenNames, resolveCssTokens } from './cssTokens.ts';
10
+ import { drawingMarkup } from './drawingMarkup.ts';
11
+ import { figureHtml } from './figureHtml.ts';
11
12
  import type { FigureLegendMark, FigureLegendText } from './figureLegend.ts';
12
13
  import { figureLegendMarkup } from './figureLegend.ts';
13
14
  import type { FigurePiece } from './figureSvgDocument.ts';
@@ -17,6 +18,7 @@ import {
17
18
  figureDrawings,
18
19
  figureElement,
19
20
  figureLegends,
21
+ figurePaintings,
20
22
  } from './figureTarget.ts';
21
23
 
22
24
  /** How the figure is copied. */
@@ -74,11 +76,13 @@ export function figureSvg(
74
76
  ): FigureSvg {
75
77
  const element = figureElement(target);
76
78
  const drawings = figureDrawings(element);
77
- if (drawings.length === 0) {
79
+ const paintings = figurePaintings(element);
80
+ if (drawings.length === 0 && paintings.length === 0) {
78
81
  throw new Error('That part of the page holds no figure to save.');
79
82
  }
80
83
 
81
- const bounds = figureBounds(drawings);
84
+ const bounds = figureBounds([...drawings, ...paintings]);
85
+ const styles = window.getComputedStyle(element);
82
86
  const pieces: FigurePiece[] = [];
83
87
  for (const drawing of drawings) {
84
88
  const box = drawing.getBoundingClientRect();
@@ -89,6 +93,16 @@ export function figureSvg(
89
93
  });
90
94
  }
91
95
 
96
+ for (const part of paintings) {
97
+ const box = part.getBoundingClientRect();
98
+ pieces.push({
99
+ markup: figureHtml(part, styles.fontFamily),
100
+ x: box.left - bounds.left,
101
+ y: box.top - bounds.top,
102
+ kind: 'html',
103
+ });
104
+ }
105
+
92
106
  for (const legend of figureLegends(element)) {
93
107
  const box = legend.getBoundingClientRect();
94
108
  if (box.width <= 0 || box.height <= 0) continue;
@@ -100,7 +114,6 @@ export function figureSvg(
100
114
  });
101
115
  }
102
116
 
103
- const styles = window.getComputedStyle(element);
104
117
  const markup = figureSvgDocument(pieces, {
105
118
  width: bounds.width,
106
119
  height: bounds.height,
@@ -204,40 +217,6 @@ function textRuns(legend: Element): TextRun[] {
204
217
  return runs;
205
218
  }
206
219
 
207
- /**
208
- * One drawing, serialized with every token it names written out.
209
- *
210
- * The copy is taken first and read from the original, because the values are a
211
- * property of where the figure sits on the page: a detached clone inherits
212
- * nothing and would resolve every token to an empty string.
213
- * @param drawing - The drawing, as it sits on the page.
214
- * @returns Its markup.
215
- */
216
- function drawingMarkup(drawing: SVGSVGElement): string {
217
- const clone = drawing.cloneNode(true) as SVGSVGElement;
218
- const markup = new XMLSerializer().serializeToString(clone);
219
- return resolveCssTokens(markup, tokenValues(drawing, cssTokenNames(markup)));
220
- }
221
-
222
- /**
223
- * What each token the markup names is worth where the figure is drawn.
224
- * @param drawing - The drawing, as it sits on the page.
225
- * @param names - The tokens it asks for.
226
- * @returns The values, keyed by token name.
227
- */
228
- function tokenValues(
229
- drawing: SVGSVGElement,
230
- names: readonly string[],
231
- ): Record<string, string> {
232
- const values: Record<string, string> = {};
233
- if (names.length === 0) return values;
234
- const styles = window.getComputedStyle(drawing);
235
- for (const name of names) {
236
- values[name] = styles.getPropertyValue(name);
237
- }
238
- return values;
239
- }
240
-
241
220
  /**
242
221
  * The ground the figure is standing on.
243
222
  * @param styles - The computed style of the box it is mounted in.
@@ -22,11 +22,11 @@ export interface FigurePiece {
22
22
  y: number;
23
23
  /**
24
24
  * What it is. Written into the file, so a reader — a test, or somebody
25
- * editing the figure afterwards — can tell the drawings from the key that
26
- * was painted over them.
25
+ * editing the figure afterwards — can tell the drawings from a part the page
26
+ * drew in HTML and from the key that was painted over them.
27
27
  * @default 'drawing'
28
28
  */
29
- kind?: 'drawing' | 'legend';
29
+ kind?: 'drawing' | 'html' | 'legend';
30
30
  }
31
31
 
32
32
  /** How the file around the drawings is written. */
@@ -16,6 +16,9 @@ const CHROME = '[data-figure="chrome"]';
16
16
  /** A key floating over a figure, which is. */
17
17
  const LEGEND = '[data-figure="legend"]';
18
18
 
19
+ /** A part of the figure the page draws in HTML rather than in SVG. */
20
+ const HTML = '[data-figure="html"]';
21
+
19
22
  /** A figure's box on the page, in the coordinates a bounding box reports. */
20
23
  export interface FigureBounds {
21
24
  /** Its left edge. */
@@ -54,7 +57,8 @@ export function figureElement(target: string | Element): Element {
54
57
  * over a figure are `<svg>` too, so anything inside an element marked as
55
58
  * chrome is left behind — a cog saved into the middle of a scatter plot is the
56
59
  * bug this exists to prevent. A drawing with no area is left behind as well,
57
- * because a tab that is not showing is still in the document.
60
+ * because a tab that is not showing is still in the document, and so is one
61
+ * inside a part drawn in HTML, which paints its own.
58
62
  * @param element - The box the figure is mounted in.
59
63
  * @returns The drawings.
60
64
  */
@@ -65,6 +69,7 @@ export function figureDrawings(element: Element): readonly SVGSVGElement[] {
65
69
  const drawings: SVGSVGElement[] = [];
66
70
  for (const drawing of element.querySelectorAll('svg')) {
67
71
  if (drawing.closest(CHROME) !== null) continue;
72
+ if (drawing.closest(HTML) !== null) continue;
68
73
  if (isNested(drawings, drawing)) continue;
69
74
  const box = drawing.getBoundingClientRect();
70
75
  if (box.width <= 0 || box.height <= 0) continue;
@@ -73,6 +78,26 @@ export function figureDrawings(element: Element): readonly SVGSVGElement[] {
73
78
  return drawings;
74
79
  }
75
80
 
81
+ /**
82
+ * The parts of it drawn in HTML: a table of buttons, a grid of cards — anything
83
+ * that is a picture on the page without being an `<svg>`. A component opts in
84
+ * by marking its box `data-figure="html"`, and the part is then painted into
85
+ * the file from where the browser put each box and each line of words.
86
+ * @param element - The box the figure is mounted in.
87
+ * @returns The parts, outermost only, in the order they are painted.
88
+ */
89
+ export function figurePaintings(element: Element): readonly Element[] {
90
+ if (element.matches(HTML)) return hasArea(element) ? [element] : [];
91
+ const parts: Element[] = [];
92
+ for (const part of element.querySelectorAll(HTML)) {
93
+ if (part.closest(CHROME) !== null) continue;
94
+ if (isNested(parts, part)) continue;
95
+ if (!hasArea(part)) continue;
96
+ parts.push(part);
97
+ }
98
+ return parts;
99
+ }
100
+
76
101
  /**
77
102
  * The keys inside it.
78
103
  *
@@ -86,6 +111,7 @@ export function figureDrawings(element: Element): readonly SVGSVGElement[] {
86
111
  export function figureLegends(element: Element): readonly Element[] {
87
112
  const legends: Element[] = [];
88
113
  for (const legend of element.querySelectorAll(LEGEND)) {
114
+ if (legend.closest(HTML) !== null) continue;
89
115
  legends.push(legend);
90
116
  }
91
117
  return legends;
@@ -93,10 +119,11 @@ export function figureLegends(element: Element): readonly Element[] {
93
119
 
94
120
  /**
95
121
  * The box every drawing of a figure fits in.
96
- * @param drawings - The drawings, as they sit on the page.
122
+ * @param drawings - The drawings, and the parts drawn in HTML, as they sit on
123
+ * the page.
97
124
  * @returns The box, or one with no area where there are no drawings.
98
125
  */
99
- export function figureBounds(drawings: readonly SVGSVGElement[]): FigureBounds {
126
+ export function figureBounds(drawings: readonly Element[]): FigureBounds {
100
127
  let left = Number.POSITIVE_INFINITY;
101
128
  let top = Number.POSITIVE_INFINITY;
102
129
  let right = Number.NEGATIVE_INFINITY;
@@ -123,7 +150,11 @@ export function figureBounds(drawings: readonly SVGSVGElement[]): FigureBounds {
123
150
  export function figureSize(target: string | Element): FigurePixels | null {
124
151
  let bounds: FigureBounds;
125
152
  try {
126
- bounds = figureBounds(figureDrawings(figureElement(target)));
153
+ const element = figureElement(target);
154
+ bounds = figureBounds([
155
+ ...figureDrawings(element),
156
+ ...figurePaintings(element),
157
+ ]);
127
158
  } catch {
128
159
  return null;
129
160
  }
@@ -140,12 +171,14 @@ export function figureSize(target: string | Element): FigurePixels | null {
140
171
  * @param drawing - The one being considered.
141
172
  * @returns Whether it is already part of the figure.
142
173
  */
143
- function isNested(
144
- taken: readonly SVGSVGElement[],
145
- drawing: SVGSVGElement,
146
- ): boolean {
174
+ function isNested(taken: readonly Element[], drawing: Element): boolean {
147
175
  for (const other of taken) {
148
176
  if (other.contains(drawing)) return true;
149
177
  }
150
178
  return false;
151
179
  }
180
+
181
+ function hasArea(element: Element): boolean {
182
+ const box = element.getBoundingClientRect();
183
+ return box.width > 0 && box.height > 0;
184
+ }
@@ -47,6 +47,7 @@ export {
47
47
  figureDrawings,
48
48
  figureElement,
49
49
  figureLegends,
50
+ figurePaintings,
50
51
  figureSize,
51
52
  } from './figureTarget.ts';
52
53
  export { sanitizeFileName } from './sanitizeFileName.ts';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Where the browser broke a run of words into lines, measured off the page.
3
+ */
4
+
5
+ /** One line of a run of words, as the browser broke it. */
6
+ export interface LineBox {
7
+ /** Offset of its first character in the run. */
8
+ start: number;
9
+ /** Offset just past its last character. */
10
+ end: number;
11
+ /** Its left edge on the page. */
12
+ left: number;
13
+ /** Its top edge. */
14
+ top: number;
15
+ /** Its right edge. */
16
+ right: number;
17
+ /** Its bottom edge. */
18
+ bottom: number;
19
+ }
20
+
21
+ /**
22
+ * The lines a run of words was broken into, read one character at a time:
23
+ * a character starts a new line when it sits below the middle of the last.
24
+ * @param node - The run.
25
+ * @param range - A range to measure with.
26
+ * @returns The lines, in the order they are read.
27
+ */
28
+ export function lineBoxes(node: Text, range: Range): LineBox[] {
29
+ const lines: LineBox[] = [];
30
+ const { data } = node;
31
+ let line: LineBox | undefined;
32
+ let start = 0;
33
+ while (start < data.length) {
34
+ const end = isAstral(data.codePointAt(start) ?? 0)
35
+ ? Math.min(start + 2, data.length)
36
+ : start + 1;
37
+ range.setStart(node, start);
38
+ range.setEnd(node, end);
39
+ const rect = range.getBoundingClientRect();
40
+ if (rect.width > 0 && rect.height > 0) {
41
+ if (line === undefined || rect.top + rect.height / 2 > line.bottom) {
42
+ line = {
43
+ start,
44
+ end,
45
+ left: rect.left,
46
+ top: rect.top,
47
+ right: rect.right,
48
+ bottom: rect.bottom,
49
+ };
50
+ lines.push(line);
51
+ } else {
52
+ line.end = end;
53
+ line.left = Math.min(line.left, rect.left);
54
+ line.top = Math.min(line.top, rect.top);
55
+ line.right = Math.max(line.right, rect.right);
56
+ line.bottom = Math.max(line.bottom, rect.bottom);
57
+ }
58
+ }
59
+ start = end;
60
+ }
61
+ return lines;
62
+ }
63
+
64
+ // A character outside the first plane is two UTF-16 units, measured as one.
65
+ function isAstral(code: number): boolean {
66
+ return code > 0xff_ff;
67
+ }
@@ -2,6 +2,13 @@ import type { SiteGroup, SiteGroupId } from './groups.ts';
2
2
  import { SITE_GROUPS } from './groups.ts';
3
3
  import type { EcosystemSite, SiteId, SiteRecord } from './sites.ts';
4
4
  import { ECOSYSTEM_SITES } from './sites.ts';
5
+ import { UNLISTED_SITES } from './unlistedSites.ts';
6
+
7
+ // What a site is looked up in: the listed ones, then those no menu shows yet.
8
+ const KNOWN_SITES: readonly EcosystemSite[] = [
9
+ ...ECOSYSTEM_SITES,
10
+ ...UNLISTED_SITES,
11
+ ];
5
12
 
6
13
  /** One topic and the sites written under it. */
7
14
  export interface GroupedSites {
@@ -40,13 +47,14 @@ export function sitesInGroup(id: SiteGroupId): EcosystemSite[] {
40
47
  }
41
48
 
42
49
  /**
43
- * One site of the family, by the identifier its entry carries.
50
+ * One site of the family, by the identifier its entry carries — listed in the
51
+ * menu or not yet.
44
52
  * @param id - The site being asked for.
45
53
  * @returns The site.
46
54
  * @throws {Error} When no site of the family carries that identifier.
47
55
  */
48
56
  export function siteById(id: SiteId): EcosystemSite {
49
- for (const site of ECOSYSTEM_SITES) {
57
+ for (const site of KNOWN_SITES) {
50
58
  if (site.id === id) return site;
51
59
  }
52
60
  throw new Error(`unknown ecosystem site: ${id}`);
@@ -83,7 +91,7 @@ export function findSiteByHost(host: string): EcosystemSite | undefined {
83
91
  const wanted = normalizeHost(host);
84
92
  if (wanted === '') return undefined;
85
93
 
86
- for (const site of ECOSYSTEM_SITES) {
94
+ for (const site of KNOWN_SITES) {
87
95
  if (normalizeHost(site.host) === wanted) return site;
88
96
  }
89
97
  return undefined;
@@ -1,6 +1,6 @@
1
1
  import type { SiteGroupId } from './groups.ts';
2
2
 
3
- /** The sites the menu links to. */
3
+ /** The sites of the family, whether the menu links to them yet or not. */
4
4
  export type SiteId =
5
5
  | 'learn'
6
6
  | 'inchi'
@@ -24,12 +24,14 @@ export type SiteId =
24
24
  | '3d'
25
25
  | 'periodic-table'
26
26
  | 'database'
27
+ | 'molecules'
27
28
  | 'symmetry'
28
29
  | 'osiris'
29
30
  | 'atoms'
30
31
  | 'moles'
31
32
  | 'inorganic'
32
- | 'powerpoint';
33
+ | 'powerpoint'
34
+ | 'naturals';
33
35
 
34
36
  /** A site's name, split the way its own wordmark splits it. */
35
37
  export interface SiteName {
@@ -0,0 +1,37 @@
1
+ // tokens-ok: file — every unlisted site's two colours are declared here.
2
+ import type { EcosystemSite } from './types.ts';
3
+
4
+ /**
5
+ * Sites of the family that no other site links to yet. No menu, footer or
6
+ * grid lists them, but `siteById` finds them, so each still draws its mark,
7
+ * its wordmark and its palette from here. Listing one is moving its entry
8
+ * into `ECOSYSTEM_SITES`, under the topic it already names.
9
+ */
10
+ export const UNLISTED_SITES: readonly EcosystemSite[] = [
11
+ {
12
+ id: 'molecules',
13
+ name: { lead: 'molecules', alt: 'cheminfo', dot: true },
14
+ host: 'molecules.cheminfo.org',
15
+ tagline: 'Look up a molecule, its computed properties and its sources.',
16
+ group: 'research',
17
+ brand: '#be185d',
18
+ brandAlt: '#0f766e',
19
+ mark: { plate: '#be185d', accent: '#5eead4' },
20
+ },
21
+ {
22
+ id: 'naturals',
23
+ name: { lead: 'naturals', alt: 'cheminfo', dot: true },
24
+ host: 'naturals.cheminfo.org',
25
+ tagline: 'Natural products, and the organisms they were found in.',
26
+ group: 'research',
27
+ // Moss, the green of the plants most natural products come from, 8.5
28
+ // Oklab units from the nearest lead of the family.
29
+ brand: '#365314',
30
+ // A carotenoid orange answers it, 5.2:1 on white, and is also the plate of
31
+ // the mark.
32
+ brandAlt: '#c2410c',
33
+ // The leaf is a fresh lime, so it reads as a leaf on the orange plate and
34
+ // apart from the white ring at 16 px.
35
+ mark: { plate: '#c2410c', accent: '#a3e635' },
36
+ },
37
+ ];
@@ -385,6 +385,26 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
385
385
  <ellipse cx="16" cy="9.4" rx="10.2" ry="3.9" fill={alt} />
386
386
  </>
387
387
  ),
388
+ // A ring with one bond leading out to a dot: a molecule, and the link to the
389
+ // source that holds it. The dot carries the answering colour.
390
+ molecules: (alt) => (
391
+ <>
392
+ <path
393
+ d="M13.4 11.6 19.12 14.9 19.12 21.5 13.4 24.8 7.68 21.5 7.68 14.9Z"
394
+ fill="none"
395
+ stroke="#ffffff"
396
+ strokeWidth="2.2"
397
+ strokeLinejoin="round"
398
+ />
399
+ <path
400
+ d="M19.12 14.9 22.4 13"
401
+ stroke="#ffffff"
402
+ strokeWidth="2.2"
403
+ strokeLinecap="round"
404
+ />
405
+ <circle cx="24.6" cy="11.6" r="3.6" fill={alt} />
406
+ </>
407
+ ),
388
408
  // A tetrahedron seen from slightly above, four balls at its corners: the
389
409
  // first shape a chemist meets symmetry in. The apex carries the answering
390
410
  // colour, which is also the C3 axis every base triangle turns about.
@@ -486,4 +506,28 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
486
506
  <path d="M16 8.45 20.59 11.1V16.4L16 19.05 11.41 16.4V11.1Z" fill={alt} />
487
507
  </>
488
508
  ),
509
+ // A ring, the compound, with a stem leading out of it to a leaf: a natural
510
+ // product, and the organism it came from. The leaf carries the answering
511
+ // colour.
512
+ naturals: (alt) => (
513
+ <>
514
+ <path
515
+ d="M11.5 14 16.7 17 16.7 23 11.5 26 6.3 23 6.3 17Z"
516
+ fill="none"
517
+ stroke="#ffffff"
518
+ strokeWidth="2.2"
519
+ strokeLinejoin="round"
520
+ />
521
+ <path
522
+ d="M16.7 17 19.6 14.2"
523
+ stroke="#ffffff"
524
+ strokeWidth="2.2"
525
+ strokeLinecap="round"
526
+ />
527
+ <path
528
+ d="M19 14.8C18.2 9.6 21.6 5.9 27.2 5.2 27.7 10.9 24.5 14.6 19 14.8Z"
529
+ fill={alt}
530
+ />
531
+ </>
532
+ ),
489
533
  };
@@ -168,7 +168,12 @@ export function zoomGestures(
168
168
  tool: ZoomDragMode = 'xAxis',
169
169
  ): Required<ZoomGestures> {
170
170
  const { boxZoomOnly, wheelZoom } = IR_MODE_RULES[mode];
171
- return { drag: boxZoomOnly ? 'box' : tool, wheel: wheelZoom };
171
+ // The shell owns `f`: its own command table runs `resetDomain`, which fits
172
+ // every pane of a stack rather than the one chart the caret is in, and the key
173
+ // is written down in a toolbar tooltip and in the documentation panel. A chart
174
+ // answering it as well would fit one pane and swallow the key before the
175
+ // shell heard it.
176
+ return { drag: boxZoomOnly ? 'box' : tool, wheel: wheelZoom, keys: false };
172
177
  }
173
178
 
174
179
  /**