react-cheminfo 0.4.0 → 0.6.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 (119) hide show
  1. package/README.md +182 -34
  2. package/lib/ecosystem/core/index.d.ts +1 -1
  3. package/lib/ecosystem/core/index.d.ts.map +1 -1
  4. package/lib/ecosystem/core/index.js +1 -1
  5. package/lib/ecosystem/core/index.js.map +1 -1
  6. package/lib/ecosystem/core/lookup.d.ts +11 -0
  7. package/lib/ecosystem/core/lookup.d.ts.map +1 -1
  8. package/lib/ecosystem/core/lookup.js +15 -0
  9. package/lib/ecosystem/core/lookup.js.map +1 -1
  10. package/lib/ecosystem/core/sites.d.ts +1 -1
  11. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  12. package/lib/ecosystem/core/sites.js +14 -4
  13. package/lib/ecosystem/core/sites.js.map +1 -1
  14. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  15. package/lib/ecosystem/ui/glyphs.js +4 -1
  16. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  17. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +5 -0
  18. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
  19. package/lib/orbital/ui/AtomicOrbitalCanvas.js +4 -3
  20. package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
  21. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +5 -0
  22. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
  23. package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
  24. package/lib/orbital/ui/axesGeometry.d.ts +27 -0
  25. package/lib/orbital/ui/axesGeometry.d.ts.map +1 -0
  26. package/lib/orbital/ui/axesGeometry.js +74 -0
  27. package/lib/orbital/ui/axesGeometry.js.map +1 -0
  28. package/lib/orbital/ui/camera.d.ts +7 -0
  29. package/lib/orbital/ui/camera.d.ts.map +1 -1
  30. package/lib/orbital/ui/camera.js +8 -1
  31. package/lib/orbital/ui/camera.js.map +1 -1
  32. package/lib/orbital/ui/renderAxes.d.ts +53 -0
  33. package/lib/orbital/ui/renderAxes.d.ts.map +1 -0
  34. package/lib/orbital/ui/renderAxes.js +110 -0
  35. package/lib/orbital/ui/renderAxes.js.map +1 -0
  36. package/lib/orbital/ui/viewer.d.ts +16 -0
  37. package/lib/orbital/ui/viewer.d.ts.map +1 -1
  38. package/lib/orbital/ui/viewer.js +24 -2
  39. package/lib/orbital/ui/viewer.js.map +1 -1
  40. package/lib/seo/core/documentMeta.d.ts +2 -2
  41. package/lib/seo/core/documentMeta.js +4 -3
  42. package/lib/seo/core/documentMeta.js.map +1 -1
  43. package/lib/seo/core/index.d.ts +15 -0
  44. package/lib/seo/core/index.d.ts.map +1 -1
  45. package/lib/seo/core/index.js +8 -0
  46. package/lib/seo/core/index.js.map +1 -1
  47. package/lib/seo/core/noscript.d.ts +97 -0
  48. package/lib/seo/core/noscript.d.ts.map +1 -0
  49. package/lib/seo/core/noscript.js +93 -0
  50. package/lib/seo/core/noscript.js.map +1 -0
  51. package/lib/seo/core/pageMeta.d.ts +76 -0
  52. package/lib/seo/core/pageMeta.d.ts.map +1 -0
  53. package/lib/seo/core/pageMeta.js +88 -0
  54. package/lib/seo/core/pageMeta.js.map +1 -0
  55. package/lib/seo/core/robots.d.ts +55 -0
  56. package/lib/seo/core/robots.d.ts.map +1 -0
  57. package/lib/seo/core/robots.js +70 -0
  58. package/lib/seo/core/robots.js.map +1 -0
  59. package/lib/seo/core/routes.d.ts +120 -0
  60. package/lib/seo/core/routes.d.ts.map +1 -0
  61. package/lib/seo/core/routes.js +188 -0
  62. package/lib/seo/core/routes.js.map +1 -0
  63. package/lib/seo/core/siteFiles.d.ts +69 -0
  64. package/lib/seo/core/siteFiles.d.ts.map +1 -0
  65. package/lib/seo/core/siteFiles.js +84 -0
  66. package/lib/seo/core/siteFiles.js.map +1 -0
  67. package/lib/seo/core/startDocumentMeta.d.ts +44 -0
  68. package/lib/seo/core/startDocumentMeta.d.ts.map +1 -0
  69. package/lib/seo/core/startDocumentMeta.js +47 -0
  70. package/lib/seo/core/startDocumentMeta.js.map +1 -0
  71. package/lib/seo/core/structuredData.d.ts +48 -0
  72. package/lib/seo/core/structuredData.d.ts.map +1 -0
  73. package/lib/seo/core/structuredData.js +41 -0
  74. package/lib/seo/core/structuredData.js.map +1 -0
  75. package/lib/seo/core/template.d.ts +48 -0
  76. package/lib/seo/core/template.d.ts.map +1 -0
  77. package/lib/seo/core/template.js +53 -0
  78. package/lib/seo/core/template.js.map +1 -0
  79. package/lib/seo/vite/index.d.ts +5 -0
  80. package/lib/seo/vite/index.d.ts.map +1 -0
  81. package/lib/seo/vite/index.js +3 -0
  82. package/lib/seo/vite/index.js.map +1 -0
  83. package/lib/seo/vite/ogCard.d.ts +41 -0
  84. package/lib/seo/vite/ogCard.d.ts.map +1 -0
  85. package/lib/seo/vite/ogCard.js +82 -0
  86. package/lib/seo/vite/ogCard.js.map +1 -0
  87. package/lib/seo/vite/prerender.d.ts +89 -0
  88. package/lib/seo/vite/prerender.d.ts.map +1 -0
  89. package/lib/seo/vite/prerender.js +114 -0
  90. package/lib/seo/vite/prerender.js.map +1 -0
  91. package/lib/vite.d.ts +2 -0
  92. package/lib/vite.d.ts.map +1 -0
  93. package/lib/vite.js +2 -0
  94. package/lib/vite.js.map +1 -0
  95. package/package.json +7 -2
  96. package/src/ecosystem/core/index.ts +1 -1
  97. package/src/ecosystem/core/lookup.ts +16 -0
  98. package/src/ecosystem/core/sites.ts +16 -5
  99. package/src/ecosystem/ui/glyphs.tsx +20 -1
  100. package/src/orbital/ui/AtomicOrbitalCanvas.tsx +9 -2
  101. package/src/orbital/ui/AtomicOrbitalViewer.tsx +5 -0
  102. package/src/orbital/ui/axesGeometry.ts +91 -0
  103. package/src/orbital/ui/camera.ts +9 -1
  104. package/src/orbital/ui/renderAxes.ts +190 -0
  105. package/src/orbital/ui/viewer.ts +32 -2
  106. package/src/seo/core/documentMeta.ts +5 -5
  107. package/src/seo/core/index.ts +27 -0
  108. package/src/seo/core/noscript.ts +195 -0
  109. package/src/seo/core/pageMeta.ts +128 -0
  110. package/src/seo/core/robots.ts +114 -0
  111. package/src/seo/core/routes.ts +246 -0
  112. package/src/seo/core/siteFiles.ts +112 -0
  113. package/src/seo/core/startDocumentMeta.ts +77 -0
  114. package/src/seo/core/structuredData.ts +80 -0
  115. package/src/seo/core/template.ts +54 -0
  116. package/src/seo/vite/index.ts +4 -0
  117. package/src/seo/vite/ogCard.ts +102 -0
  118. package/src/seo/vite/prerender.ts +203 -0
  119. package/src/vite.ts +1 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Where a built page lets its head and its body be written.
3
+ *
4
+ * Every address of a site needs its own title, description, canonical and card,
5
+ * and a static build ships one `index.html`. Rather than look for the tags a
6
+ * page already carries and operate on them, the template says where they go:
7
+ * two comments, replaced by what the build or the server writes for the address
8
+ * being answered.
9
+ *
10
+ * ```html
11
+ * <head>
12
+ * <meta charset="utf-8" />
13
+ * <link rel="icon" href="%BASE_URL%favicon.svg" />
14
+ * <!--cheminfo:head-->
15
+ * </head>
16
+ * <body>
17
+ * <div id="root"></div>
18
+ * <!--cheminfo:body-->
19
+ * </body>
20
+ * ```
21
+ *
22
+ * The template carries no title and no description of its own, so nothing can
23
+ * be duplicated and nothing has to be taken back out. Nothing is parsed and
24
+ * nothing is searched for but the marker, so what the rest of the page holds — a
25
+ * byte order mark, an implicit head, a `</head>` its prose displays or a script
26
+ * quotes, an unterminated comment — cannot reach the result.
27
+ */
28
+ /** Where the head a crawler reads is written. */
29
+ export const PAGE_HEAD_MARKER = '<!--cheminfo:head-->';
30
+ /** Where the crawl path a visitor with no JavaScript reads is written. */
31
+ export const PAGE_BODY_MARKER = '<!--cheminfo:body-->';
32
+ /**
33
+ * Write content in the place the template kept for it.
34
+ *
35
+ * The marker is consumed, so a page is always filled from the template and
36
+ * never from a filled page: applying this twice throws rather than writing a
37
+ * second head, which is why idempotence is not something the caller has to
38
+ * defend.
39
+ * @param html - The template.
40
+ * @param marker - {@link PAGE_HEAD_MARKER} or {@link PAGE_BODY_MARKER}.
41
+ * @param content - The markup to write, taken as written: no `$&`, `$1` or
42
+ * `$<name>` is expanded.
43
+ * @returns The page.
44
+ * @throws {Error} When the template carries no such marker, rather than
45
+ * silently shipping a page with no head.
46
+ */
47
+ export function fill(html, marker, content) {
48
+ const at = html.indexOf(marker);
49
+ if (at === -1)
50
+ throw new Error(`the page carries no ${marker}`);
51
+ return html.slice(0, at) + content + html.slice(at + marker.length);
52
+ }
53
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/seo/core/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,iDAAiD;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,MAAc,EAAE,OAAe;IAChE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,5 @@
1
+ export type { OgCardOptions } from './ogCard.ts';
2
+ export { OG_HEIGHT, OG_WIDTH, ogCardHtml } from './ogCard.ts';
3
+ export type { PrerenderOptions } from './prerender.ts';
4
+ export { cheminfoPrerender } from './prerender.ts';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seo/vite/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { OG_HEIGHT, OG_WIDTH, ogCardHtml } from "./ogCard.js";
2
+ export { cheminfoPrerender } from "./prerender.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seo/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The 1200×630 card a link to a site unfurls into, as a page to screenshot.
3
+ *
4
+ * The card is the site's own mark, its two colours and its name, all read from
5
+ * its record — so it is generated rather than hand-drawn. A mark redrawn in the
6
+ * card is a mark that drifts from the one the site shows.
7
+ *
8
+ * React is reached for only when a card is actually drawn, and never at module
9
+ * load: this module sits in the same entry point as the prerender plugin, which
10
+ * a site must be able to import with none of the optional peers installed.
11
+ */
12
+ import type { EcosystemSite, SiteId } from '../../ecosystem/core/sites.ts';
13
+ /** The width every card is drawn at. */
14
+ export declare const OG_WIDTH = 1200;
15
+ /** The height every card is drawn at. */
16
+ export declare const OG_HEIGHT = 630;
17
+ /** What the card says, beyond the site's own name and mark. */
18
+ export interface OgCardOptions {
19
+ /** The site, named or passed. */
20
+ site: EcosystemSite | SiteId;
21
+ /**
22
+ * The sentence under the name.
23
+ * @default the site's tagline
24
+ */
25
+ description?: string;
26
+ }
27
+ /**
28
+ * The card, as a standalone page.
29
+ *
30
+ * Screenshot it at {@link OG_WIDTH} × {@link OG_HEIGHT} — a headless browser is
31
+ * the only thing here that can rasterise it, and every site already has one for
32
+ * its end-to-end tests.
33
+ *
34
+ * Drawing the mark is the one thing here that needs React, so React is loaded
35
+ * on the call rather than by the module. The card is drawn by a build script
36
+ * that already awaits a browser, so the await costs it nothing.
37
+ * @param options - Which site, and what it says.
38
+ * @returns A complete HTML document.
39
+ */
40
+ export declare function ogCardHtml(options: OgCardOptions): Promise<string>;
41
+ //# sourceMappingURL=ogCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ogCard.d.ts","sourceRoot":"","sources":["../../../src/seo/vite/ogCard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG3E,wCAAwC;AACxC,eAAO,MAAM,QAAQ,OAAO,CAAC;AAE7B,yCAAyC;AACzC,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDxE"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The 1200×630 card a link to a site unfurls into, as a page to screenshot.
3
+ *
4
+ * The card is the site's own mark, its two colours and its name, all read from
5
+ * its record — so it is generated rather than hand-drawn. A mark redrawn in the
6
+ * card is a mark that drifts from the one the site shows.
7
+ *
8
+ * React is reached for only when a card is actually drawn, and never at module
9
+ * load: this module sits in the same entry point as the prerender plugin, which
10
+ * a site must be able to import with none of the optional peers installed.
11
+ */
12
+ import { siteById } from "../../ecosystem/core/lookup.js";
13
+ import { escapeText } from "../../share/core/escape.js";
14
+ /** The width every card is drawn at. */
15
+ export const OG_WIDTH = 1200;
16
+ /** The height every card is drawn at. */
17
+ export const OG_HEIGHT = 630;
18
+ /**
19
+ * The card, as a standalone page.
20
+ *
21
+ * Screenshot it at {@link OG_WIDTH} × {@link OG_HEIGHT} — a headless browser is
22
+ * the only thing here that can rasterise it, and every site already has one for
23
+ * its end-to-end tests.
24
+ *
25
+ * Drawing the mark is the one thing here that needs React, so React is loaded
26
+ * on the call rather than by the module. The card is drawn by a build script
27
+ * that already awaits a browser, so the await costs it nothing.
28
+ * @param options - Which site, and what it says.
29
+ * @returns A complete HTML document.
30
+ */
31
+ export async function ogCardHtml(options) {
32
+ const site = typeof options.site === 'string' ? siteById(options.site) : options.site;
33
+ const description = options.description ?? site.tagline;
34
+ const [{ createElement }, { renderToStaticMarkup }, { SiteMark }] = await Promise.all([
35
+ import('react'),
36
+ import('react-dom/server'),
37
+ import("../../ecosystem/ui/marks.js"),
38
+ ]);
39
+ const mark = renderToStaticMarkup(createElement(SiteMark, { site, size: 132, colors: 'literal' }));
40
+ const dot = site.name.dot === true ? '<span class="dot">.</span>' : '';
41
+ return `<!doctype html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="utf-8" />
45
+ <style>
46
+ * { box-sizing: border-box; margin: 0; }
47
+ body {
48
+ display: flex;
49
+ width: ${OG_WIDTH}px;
50
+ height: ${OG_HEIGHT}px;
51
+ flex-direction: column;
52
+ justify-content: center;
53
+ padding: 88px;
54
+ background: #ffffff;
55
+ color: #16202c;
56
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
57
+ Helvetica, Arial, sans-serif;
58
+ gap: 28px;
59
+ }
60
+ h1 { font-size: 76px; font-weight: 700; letter-spacing: -0.02em; }
61
+ .lead { color: ${site.brand}; }
62
+ .alt { color: ${site.brandAlt}; }
63
+ .dot { color: #8a96a3; }
64
+ p { max-width: 900px; color: #5b6875; font-size: 34px; line-height: 1.35; }
65
+ .rule {
66
+ width: 180px;
67
+ height: 10px;
68
+ border-radius: 5px;
69
+ background: ${site.brandAlt};
70
+ }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ ${mark}
75
+ <h1><span class="lead">${escapeText(site.name.lead)}</span>${dot}<span class="alt">${escapeText(site.name.alt)}</span></h1>
76
+ <div class="rule"></div>
77
+ <p>${escapeText(description)}</p>
78
+ </body>
79
+ </html>
80
+ `;
81
+ }
82
+ //# sourceMappingURL=ogCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ogCard.js","sourceRoot":"","sources":["../../../src/seo/vite/ogCard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAExD,wCAAwC;AACxC,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC;AAE7B,yCAAyC;AACzC,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAa7B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAsB;IACrD,MAAM,IAAI,GACR,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC;IACxD,MAAM,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,oBAAoB,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,GAC/D,MAAM,OAAO,CAAC,GAAG,CAAC;QAChB,MAAM,CAAC,OAAO,CAAC;QACf,MAAM,CAAC,kBAAkB,CAAC;QAC1B,MAAM,CAAC,6BAA8B,CAAC;KACvC,CAAC,CAAC;IACL,MAAM,IAAI,GAAG,oBAAoB,CAC/B,aAAa,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAChE,CAAC;IACF,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,OAAO;;;;;;;;iBAQQ,QAAQ;kBACP,SAAS;;;;;;;;;;;uBAWJ,IAAI,CAAC,KAAK;sBACX,IAAI,CAAC,QAAQ;;;;;;;sBAOb,IAAI,CAAC,QAAQ;;;;;MAK7B,IAAI;6BACmB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,qBAAqB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;;SAEzG,UAAU,CAAC,WAAW,CAAC;;;CAG/B,CAAC;AACF,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Write one real HTML file per routed address, and everything else a crawler
3
+ * fetches on its own.
4
+ *
5
+ * A site served by a static image has nothing to rewrite a head per request: a
6
+ * crawler gets whatever came off the wire. Without this every address carries
7
+ * the same title and a search engine folds the whole site into one result.
8
+ *
9
+ * The build's `index.html` is the template: it declares where its head and its
10
+ * crawl path go with `<!--cheminfo:head-->` and `<!--cheminfo:body-->`, and
11
+ * every file written here is filled from it. The dev server is filled the same
12
+ * way, from the home route, so what a developer opens is what the build ships.
13
+ *
14
+ * These files are also what makes the server's catch-all fallback unnecessary.
15
+ * Every address the tool answers is on disk, so an address that is *not* on
16
+ * disk is genuinely not a page and must 404 rather than serving the tool under
17
+ * a name it does not have.
18
+ */
19
+ import type { Plugin } from 'vite';
20
+ import type { EcosystemSite, SiteId } from '../../ecosystem/core/sites.ts';
21
+ import type { NoscriptText } from '../core/noscript.ts';
22
+ import type { RobotsDisallow } from '../core/robots.ts';
23
+ import type { RouteMeta } from '../core/routes.ts';
24
+ /** What the build needs to know to write the site's addresses. */
25
+ export interface PrerenderOptions {
26
+ /** The site, named or passed. */
27
+ site: EcosystemSite | SiteId;
28
+ /** Every address it answers, each with its title and description. */
29
+ routes: readonly RouteMeta[];
30
+ /**
31
+ * Where the site is served, mount path included. Every absolute address is
32
+ * built on it, and `robots.txt` and the `noscript` index write their paths
33
+ * under its mount. The files on disk are laid out from the build's own root
34
+ * either way: it is the server that puts them under the mount.
35
+ * @default `https://<the site's host>`
36
+ */
37
+ origin?: string;
38
+ /**
39
+ * Addresses `robots.txt` keeps out of the index, e.g. `/v1/`, each optionally
40
+ * with the sentence saying why. Set to `false` to write no `robots.txt` at
41
+ * all, for a site that ships its own.
42
+ * @default []
43
+ */
44
+ robots?: false | ReadonlyArray<string | RobotsDisallow>;
45
+ /**
46
+ * The schema.org category of the structured-data block, or `false` to write
47
+ * none.
48
+ * @default 'EducationalApplication'
49
+ */
50
+ category?: false | string;
51
+ /**
52
+ * What the tool needs to run, named in the structured-data block.
53
+ * @default 'Any modern browser'
54
+ */
55
+ operatingSystem?: string;
56
+ /**
57
+ * What the tool does, in the structured-data block.
58
+ * @default the site's tagline
59
+ */
60
+ description?: string;
61
+ /**
62
+ * What a browser has to offer for the tool to run.
63
+ * @default 'Requires JavaScript'
64
+ */
65
+ browserRequirements?: string;
66
+ /**
67
+ * The currency the zero price is quoted in.
68
+ * @default 'EUR'
69
+ */
70
+ currency?: string;
71
+ /**
72
+ * Whether the built page carries a `noscript` index of the addresses, what it
73
+ * says, which pages it lists and how it writes their addresses. It is the
74
+ * only crawl path through a site whose body is an empty root element, so
75
+ * `false` leaves one without any — and is also what lets a template ship no
76
+ * `<!--cheminfo:body-->` at all.
77
+ * @default true
78
+ */
79
+ noscript?: boolean | NoscriptText;
80
+ }
81
+ /**
82
+ * Prerender every routed address of a cheminfo site.
83
+ * @param options - The site, its routes, and what a crawler is told.
84
+ * @returns The Vite plugin.
85
+ * @throws {Error} When the route table names an address twice or names one that
86
+ * is not a path, or when the origin is not an absolute address.
87
+ */
88
+ export declare function cheminfoPrerender(options: PrerenderOptions): Plugin;
89
+ //# sourceMappingURL=prerender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerender.d.ts","sourceRoot":"","sources":["../../../src/seo/vite/prerender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAMnD,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,IAAI,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7B,qEAAqE;IACrE,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,KAAK,GAAG,aAAa,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;CACnC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CA6EnE"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Write one real HTML file per routed address, and everything else a crawler
3
+ * fetches on its own.
4
+ *
5
+ * A site served by a static image has nothing to rewrite a head per request: a
6
+ * crawler gets whatever came off the wire. Without this every address carries
7
+ * the same title and a search engine folds the whole site into one result.
8
+ *
9
+ * The build's `index.html` is the template: it declares where its head and its
10
+ * crawl path go with `<!--cheminfo:head-->` and `<!--cheminfo:body-->`, and
11
+ * every file written here is filled from it. The dev server is filled the same
12
+ * way, from the home route, so what a developer opens is what the build ships.
13
+ *
14
+ * These files are also what makes the server's catch-all fallback unnecessary.
15
+ * Every address the tool answers is on disk, so an address that is *not* on
16
+ * disk is genuinely not a page and must 404 rather than serving the tool under
17
+ * a name it does not have.
18
+ */
19
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { noscriptIndex } from "../core/noscript.js";
22
+ import { pageHeadTags } from "../core/pageMeta.js";
23
+ import { robotsTxt } from "../core/robots.js";
24
+ import { assertRoutes, homeRoute, trimTrailingSlash } from "../core/routes.js";
25
+ import { sitemapXml } from "../core/siteFiles.js";
26
+ import { structuredDataScript } from "../core/structuredData.js";
27
+ import { PAGE_BODY_MARKER, PAGE_HEAD_MARKER, fill } from "../core/template.js";
28
+ /**
29
+ * Prerender every routed address of a cheminfo site.
30
+ * @param options - The site, its routes, and what a crawler is told.
31
+ * @returns The Vite plugin.
32
+ * @throws {Error} When the route table names an address twice or names one that
33
+ * is not a path, or when the origin is not an absolute address.
34
+ */
35
+ export function cheminfoPrerender(options) {
36
+ const { site, routes, origin, robots = [] } = options;
37
+ assertRoutes(routes);
38
+ const structuredData = structuredDataOf(options);
39
+ const crawlPath = crawlPathOf(options);
40
+ let out = 'dist';
41
+ let serve = false;
42
+ let logger = null;
43
+ const page = (template, url) => {
44
+ const head = fill(template, PAGE_HEAD_MARKER, `${pageHeadTags({ site, routes, origin, url })}${structuredData}`);
45
+ return crawlPath === '' ? head : fill(head, PAGE_BODY_MARKER, crawlPath);
46
+ };
47
+ return {
48
+ name: 'cheminfo:prerender',
49
+ configResolved(config) {
50
+ serve = config.command === 'serve';
51
+ out = resolve(config.root, config.build.outDir);
52
+ logger = config.logger;
53
+ },
54
+ // A dev run has no build to prerender, so the page vite serves is filled
55
+ // from the home route rather than shipped with its markers showing.
56
+ transformIndexHtml: {
57
+ order: 'post',
58
+ handler: (html) => serve ? page(html, homeRoute(routes).path) : html,
59
+ },
60
+ closeBundle() {
61
+ if (serve)
62
+ return;
63
+ const template = readFileSync(join(out, 'index.html'), 'utf8');
64
+ const write = (url, file) => {
65
+ mkdirSync(dirname(file), { recursive: true });
66
+ writeFileSync(file, page(template, url));
67
+ };
68
+ let root = false;
69
+ for (const route of routes) {
70
+ const address = trimTrailingSlash(route.path);
71
+ if (address === '/')
72
+ root = true;
73
+ write(route.path, address === '/'
74
+ ? join(out, 'index.html')
75
+ : join(out, address.slice(1), 'index.html'));
76
+ }
77
+ // The file a static server hands out for the mount itself. A table naming
78
+ // no root would otherwise leave the template vite built, and ship a site
79
+ // whose front page carries its markers instead of a head.
80
+ if (!root)
81
+ write(homeRoute(routes).path, join(out, 'index.html'));
82
+ writeFileSync(join(out, 'sitemap.xml'), sitemapXml({ site, routes, origin }));
83
+ if (robots !== false) {
84
+ writeFileSync(join(out, 'robots.txt'), robotsTxt({ site, routes, origin }, robots));
85
+ }
86
+ logger?.info(`${routes.length} pages prerendered, and listed in sitemap.xml`);
87
+ },
88
+ };
89
+ }
90
+ function structuredDataOf(options) {
91
+ const { category, ...rest } = options;
92
+ if (category === false)
93
+ return '';
94
+ const { site, routes, origin } = rest;
95
+ const { operatingSystem, description, browserRequirements, currency } = rest;
96
+ return `\n${structuredDataScript({
97
+ site,
98
+ routes,
99
+ origin,
100
+ category,
101
+ operatingSystem,
102
+ description,
103
+ browserRequirements,
104
+ currency,
105
+ })}`;
106
+ }
107
+ function crawlPathOf(options) {
108
+ const { site, routes, origin, noscript = true } = options;
109
+ if (noscript === false)
110
+ return '';
111
+ const { routes: listed, ...prose } = noscript === true ? {} : noscript;
112
+ return noscriptIndex({ site, origin, ...prose, routes: listed ?? routes });
113
+ }
114
+ //# sourceMappingURL=prerender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerender.js","sourceRoot":"","sources":["../../../src/seo/vite/prerender.ts"],"names":[],"mappings":"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"}
package/lib/vite.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './seo/vite/index.ts';
2
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
package/lib/vite.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./seo/vite/index.js";
2
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "description": "Assembled React components shared by the cheminfo sites: a Cite button and an ecosystem menu",
6
6
  "author": "Luc Patiny",
@@ -16,6 +16,7 @@
16
16
  "./orbital": "./lib/orbital.js",
17
17
  "./structure": "./lib/structure.js",
18
18
  "./ui": "./lib/ui.js",
19
+ "./vite": "./lib/vite.js",
19
20
  "./styles/*.css": "./styles/*.css"
20
21
  },
21
22
  "files": [
@@ -76,7 +77,8 @@
76
77
  "react": "^18.3.1 || >=19",
77
78
  "react-dom": "^18.3.1 || >=19",
78
79
  "react-ocl": "^8.7.1",
79
- "react-science": "^20.6.2 || ^21.0.0 || ^22.0.0 || ^23.0.0"
80
+ "react-science": "^20.6.2 || ^21.0.0 || ^22.0.0 || ^23.0.0",
81
+ "vite": "^7.0.0 || ^8.0.0"
80
82
  },
81
83
  "repository": {
82
84
  "type": "git",
@@ -95,6 +97,9 @@
95
97
  },
96
98
  "react-ocl": {
97
99
  "optional": true
100
+ },
101
+ "vite": {
102
+ "optional": true
98
103
  }
99
104
  }
100
105
  }
@@ -1,5 +1,5 @@
1
1
  export { renderEcosystemLinksHtml } from './links.ts';
2
- export { findSiteByHost, siteById } from './lookup.ts';
2
+ export { findSiteByHost, siteById, siteDisplayName } from './lookup.ts';
3
3
  export type {
4
4
  EcosystemSite,
5
5
  SiteId,
@@ -14,6 +14,22 @@ export function siteById(id: SiteId): EcosystemSite {
14
14
  throw new Error(`unknown ecosystem site: ${id}`);
15
15
  }
16
16
 
17
+ /**
18
+ * The site's name as prose, for a page title, a card or a citation.
19
+ *
20
+ * An address-shaped name is written as the address it is — `3d` and `cheminfo`
21
+ * are two halves of `3d.cheminfo.org`, and joining them would read `3dcheminfo`
22
+ * — while a product-shaped one is its two halves run together, as its wordmark
23
+ * sets them.
24
+ * @param site - The site being named.
25
+ * @returns What the site is called, in one string.
26
+ */
27
+ export function siteDisplayName(site: EcosystemSite): string {
28
+ return site.name.dot === true
29
+ ? site.host
30
+ : `${site.name.lead}${site.name.alt}`;
31
+ }
32
+
17
33
  /**
18
34
  * The site a page is being served from, so a header can mark itself as the
19
35
  * current one without being told which site it belongs to.
@@ -5,6 +5,7 @@ export type SiteId =
5
5
  | 'smiles'
6
6
  | 'chemcalc'
7
7
  | 'nmrium'
8
+ | 'derepflow'
8
9
  | 'surge'
9
10
  | 'tex'
10
11
  | 'lcao'
@@ -14,7 +15,7 @@ export type SiteId =
14
15
  | 'equilibrium'
15
16
  | 'polycarp'
16
17
  | '3d'
17
- | 'pt';
18
+ | 'periodic-table';
18
19
 
19
20
  export interface SiteName {
20
21
  /** First half of the name, set in the site's leading colour. */
@@ -119,6 +120,16 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
119
120
  brandAlt: '#2b143e',
120
121
  mark: { plate: '#2b143e', accent: '#ea580c' },
121
122
  },
123
+ {
124
+ id: 'derepflow',
125
+ name: { lead: 'Derep', alt: 'Flow' },
126
+ host: 'derepflow.cheminfo.org',
127
+ repository: 'https://github.com/cheminfo/derepflow',
128
+ tagline: 'Identify the constituents of a mixture from its ¹³C spectrum.',
129
+ brand: '#be185d',
130
+ brandAlt: '#0f766e',
131
+ mark: { plate: '#be185d', accent: '#0d9488' },
132
+ },
122
133
  {
123
134
  id: 'surge',
124
135
  name: { lead: 'surge', alt: 'cheminfo', dot: true },
@@ -210,10 +221,10 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
210
221
  mark: { plate: '#155e75', accent: '#f97316' },
211
222
  },
212
223
  {
213
- id: 'pt',
214
- name: { lead: 'pt', alt: 'cheminfo', dot: true },
215
- host: 'pt.cheminfo.org',
216
- repository: 'https://github.com/cheminfo/pt.cheminfo.org',
224
+ id: 'periodic-table',
225
+ name: { lead: 'Periodic', alt: 'Table' },
226
+ host: 'periodic-table.cheminfo.org',
227
+ repository: 'https://github.com/cheminfo/periodic-table.cheminfo.org',
217
228
  tagline: 'The periodic table, coloured, plotted and taken apart.',
218
229
  brand: '#15803d',
219
230
  brandAlt: '#a16207',
@@ -74,6 +74,25 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
74
74
  <path d="M23 24V18" stroke="#ffffff" />
75
75
  </g>
76
76
  ),
77
+ // Three peaks of a mixture over the funnel that narrows them to one compound,
78
+ // which is the dereplication this one does.
79
+ derepflow: (alt) => (
80
+ <>
81
+ <g stroke="#ffffff" strokeWidth="4" strokeLinecap="round" fill="none">
82
+ <path d="M7 15.6V9.4" />
83
+ <path d="M16 15.6V4.6" />
84
+ <path d="M25 15.6v-4.6" />
85
+ </g>
86
+ <path
87
+ d="M6.2 20.4 16 27 25.8 20.4"
88
+ stroke={alt}
89
+ strokeWidth="4"
90
+ strokeLinecap="round"
91
+ strokeLinejoin="round"
92
+ fill="none"
93
+ />
94
+ </>
95
+ ),
77
96
  // The official NMRium symbol, from `Logo.tsx` of the NMRium sources: the
78
97
  // orange half is theirs, and the half their logo sets in plum is set in white
79
98
  // here because the plate already is that plum.
@@ -214,7 +233,7 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
214
233
  // The stepped corner of the table, with the one cell a visitor has picked
215
234
  // carrying the accent — which is what every tool of that site is: the table,
216
235
  // and one element read off it.
217
- pt: (alt) => (
236
+ 'periodic-table': (alt) => (
218
237
  <>
219
238
  <g fill="#ffffff" opacity="0.55">
220
239
  <rect x="5" y="6" width="5" height="5" rx="1" />
@@ -15,6 +15,7 @@ import { PHASE_PALETTES } from '../core/palette.ts';
15
15
  import type { AtomicSampler } from '../core/sample.ts';
16
16
  import { sampleInProcess } from '../core/sample.ts';
17
17
 
18
+ import { DEFAULT_SPIN_SPEED } from './camera.ts';
18
19
  import type { OrbitalViewer } from './viewer.ts';
19
20
  import { createOrbitalViewer } from './viewer.ts';
20
21
 
@@ -44,6 +45,11 @@ export interface AtomicOrbitalCanvasProps {
44
45
  * @default false
45
46
  */
46
47
  spinning?: boolean;
48
+ /**
49
+ * How fast it turns, in molstar's own spin unit. Lower is slower.
50
+ * @default 0.3
51
+ */
52
+ spinSpeed?: number;
47
53
  /**
48
54
  * How the field is produced. Supply a worker-backed sampler to keep the main
49
55
  * thread free; the default runs in process.
@@ -77,6 +83,7 @@ export function AtomicOrbitalCanvas(
77
83
  palette = PHASE_PALETTES.textbook,
78
84
  resolution = DEFAULT_RESOLUTION,
79
85
  spinning = false,
86
+ spinSpeed = DEFAULT_SPIN_SPEED,
80
87
  sample = sampleInProcess,
81
88
  onNodeRadii,
82
89
  onError,
@@ -144,8 +151,8 @@ export function AtomicOrbitalCanvas(
144
151
  }, [atomicNumber, orbitalId, resolution, palette, sample, wanted]);
145
152
 
146
153
  useEffect(() => {
147
- void viewerRef.current?.setSpin(spinning);
148
- }, [spinning]);
154
+ void viewerRef.current?.setSpin(spinning, spinSpeed);
155
+ }, [spinning, spinSpeed]);
149
156
 
150
157
  return (
151
158
  <div ref={containerRef} style={CANVAS_STYLE}>
@@ -56,6 +56,11 @@ export interface AtomicOrbitalViewerProps {
56
56
  * @default false
57
57
  */
58
58
  spinning?: boolean;
59
+ /**
60
+ * How fast it turns, in molstar's own spin unit. Lower is slower.
61
+ * @default 0.3
62
+ */
63
+ spinSpeed?: number;
59
64
  /**
60
65
  * How the field is produced. Supply a worker-backed sampler to keep the main
61
66
  * thread free; the default runs in process.