react-cheminfo 0.4.1 → 0.7.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 (174) hide show
  1. package/README.md +198 -34
  2. package/lib/citation/core/index.d.ts +1 -0
  3. package/lib/citation/core/index.d.ts.map +1 -1
  4. package/lib/citation/core/index.js +1 -0
  5. package/lib/citation/core/index.js.map +1 -1
  6. package/lib/citation/core/platformPaper.d.ts +14 -0
  7. package/lib/citation/core/platformPaper.d.ts.map +1 -0
  8. package/lib/citation/core/platformPaper.js +28 -0
  9. package/lib/citation/core/platformPaper.js.map +1 -0
  10. package/lib/color/core/index.d.ts +1 -1
  11. package/lib/color/core/index.d.ts.map +1 -1
  12. package/lib/color/core/index.js +1 -1
  13. package/lib/color/core/index.js.map +1 -1
  14. package/lib/core.d.ts +1 -0
  15. package/lib/core.d.ts.map +1 -1
  16. package/lib/core.js +1 -0
  17. package/lib/core.js.map +1 -1
  18. package/lib/ecosystem/core/sites.d.ts +1 -1
  19. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  20. package/lib/ecosystem/core/sites.js +15 -5
  21. package/lib/ecosystem/core/sites.js.map +1 -1
  22. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  23. package/lib/ecosystem/ui/glyphs.js +3 -0
  24. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  25. package/lib/orbital/core/index.d.ts +1 -1
  26. package/lib/orbital/core/index.d.ts.map +1 -1
  27. package/lib/orbital/core/index.js +1 -1
  28. package/lib/orbital/core/index.js.map +1 -1
  29. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +5 -0
  30. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
  31. package/lib/orbital/ui/AtomicOrbitalCanvas.js +4 -3
  32. package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
  33. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +5 -0
  34. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
  35. package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
  36. package/lib/orbital/ui/axesGeometry.d.ts +27 -0
  37. package/lib/orbital/ui/axesGeometry.d.ts.map +1 -0
  38. package/lib/orbital/ui/axesGeometry.js +74 -0
  39. package/lib/orbital/ui/axesGeometry.js.map +1 -0
  40. package/lib/orbital/ui/camera.d.ts +7 -0
  41. package/lib/orbital/ui/camera.d.ts.map +1 -1
  42. package/lib/orbital/ui/camera.js +8 -1
  43. package/lib/orbital/ui/camera.js.map +1 -1
  44. package/lib/orbital/ui/renderAxes.d.ts +53 -0
  45. package/lib/orbital/ui/renderAxes.d.ts.map +1 -0
  46. package/lib/orbital/ui/renderAxes.js +110 -0
  47. package/lib/orbital/ui/renderAxes.js.map +1 -0
  48. package/lib/orbital/ui/viewer.d.ts +16 -0
  49. package/lib/orbital/ui/viewer.d.ts.map +1 -1
  50. package/lib/orbital/ui/viewer.js +24 -2
  51. package/lib/orbital/ui/viewer.js.map +1 -1
  52. package/lib/periodic/core/categories.d.ts +25 -0
  53. package/lib/periodic/core/categories.d.ts.map +1 -0
  54. package/lib/periodic/core/categories.js +70 -0
  55. package/lib/periodic/core/categories.js.map +1 -0
  56. package/lib/periodic/core/elements.d.ts +49 -0
  57. package/lib/periodic/core/elements.d.ts.map +1 -0
  58. package/lib/periodic/core/elements.js +175 -0
  59. package/lib/periodic/core/elements.js.map +1 -0
  60. package/lib/periodic/core/index.d.ts +6 -0
  61. package/lib/periodic/core/index.d.ts.map +1 -0
  62. package/lib/periodic/core/index.js +4 -0
  63. package/lib/periodic/core/index.js.map +1 -0
  64. package/lib/periodic/core/layout.d.ts +73 -0
  65. package/lib/periodic/core/layout.d.ts.map +1 -0
  66. package/lib/periodic/core/layout.js +132 -0
  67. package/lib/periodic/core/layout.js.map +1 -0
  68. package/lib/periodic/ui/CategoryLegend.d.ts +26 -0
  69. package/lib/periodic/ui/CategoryLegend.d.ts.map +1 -0
  70. package/lib/periodic/ui/CategoryLegend.js +53 -0
  71. package/lib/periodic/ui/CategoryLegend.js.map +1 -0
  72. package/lib/periodic/ui/ElementCell.d.ts +53 -0
  73. package/lib/periodic/ui/ElementCell.d.ts.map +1 -0
  74. package/lib/periodic/ui/ElementCell.js +56 -0
  75. package/lib/periodic/ui/ElementCell.js.map +1 -0
  76. package/lib/periodic/ui/PeriodicTable.d.ts +90 -0
  77. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -0
  78. package/lib/periodic/ui/PeriodicTable.js +77 -0
  79. package/lib/periodic/ui/PeriodicTable.js.map +1 -0
  80. package/lib/periodic/ui/PeriodicTableChrome.d.ts +36 -0
  81. package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -0
  82. package/lib/periodic/ui/PeriodicTableChrome.js +72 -0
  83. package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -0
  84. package/lib/periodic/ui/index.d.ts +7 -0
  85. package/lib/periodic/ui/index.d.ts.map +1 -0
  86. package/lib/periodic/ui/index.js +4 -0
  87. package/lib/periodic/ui/index.js.map +1 -0
  88. package/lib/seo/core/documentMeta.d.ts +2 -2
  89. package/lib/seo/core/documentMeta.js +4 -3
  90. package/lib/seo/core/documentMeta.js.map +1 -1
  91. package/lib/seo/core/index.d.ts +13 -4
  92. package/lib/seo/core/index.d.ts.map +1 -1
  93. package/lib/seo/core/index.js +8 -3
  94. package/lib/seo/core/index.js.map +1 -1
  95. package/lib/seo/core/noscript.d.ts +97 -0
  96. package/lib/seo/core/noscript.d.ts.map +1 -0
  97. package/lib/seo/core/noscript.js +93 -0
  98. package/lib/seo/core/noscript.js.map +1 -0
  99. package/lib/seo/core/pageMeta.d.ts +30 -14
  100. package/lib/seo/core/pageMeta.d.ts.map +1 -1
  101. package/lib/seo/core/pageMeta.js +40 -43
  102. package/lib/seo/core/pageMeta.js.map +1 -1
  103. package/lib/seo/core/robots.d.ts +55 -0
  104. package/lib/seo/core/robots.d.ts.map +1 -0
  105. package/lib/seo/core/robots.js +70 -0
  106. package/lib/seo/core/robots.js.map +1 -0
  107. package/lib/seo/core/routes.d.ts +73 -5
  108. package/lib/seo/core/routes.d.ts.map +1 -1
  109. package/lib/seo/core/routes.js +142 -16
  110. package/lib/seo/core/routes.js.map +1 -1
  111. package/lib/seo/core/siteFiles.d.ts +39 -43
  112. package/lib/seo/core/siteFiles.d.ts.map +1 -1
  113. package/lib/seo/core/siteFiles.js +53 -69
  114. package/lib/seo/core/siteFiles.js.map +1 -1
  115. package/lib/seo/core/startDocumentMeta.d.ts +44 -0
  116. package/lib/seo/core/startDocumentMeta.d.ts.map +1 -0
  117. package/lib/seo/core/startDocumentMeta.js +47 -0
  118. package/lib/seo/core/startDocumentMeta.js.map +1 -0
  119. package/lib/seo/core/structuredData.d.ts +48 -0
  120. package/lib/seo/core/structuredData.d.ts.map +1 -0
  121. package/lib/seo/core/structuredData.js +41 -0
  122. package/lib/seo/core/structuredData.js.map +1 -0
  123. package/lib/seo/core/template.d.ts +48 -0
  124. package/lib/seo/core/template.d.ts.map +1 -0
  125. package/lib/seo/core/template.js +53 -0
  126. package/lib/seo/core/template.js.map +1 -0
  127. package/lib/seo/vite/ogCard.d.ts +9 -1
  128. package/lib/seo/vite/ogCard.d.ts.map +1 -1
  129. package/lib/seo/vite/ogCard.js +14 -4
  130. package/lib/seo/vite/ogCard.js.map +1 -1
  131. package/lib/seo/vite/prerender.d.ts +38 -7
  132. package/lib/seo/vite/prerender.d.ts.map +1 -1
  133. package/lib/seo/vite/prerender.js +68 -30
  134. package/lib/seo/vite/prerender.js.map +1 -1
  135. package/lib/ui.d.ts +1 -0
  136. package/lib/ui.d.ts.map +1 -1
  137. package/lib/ui.js +1 -0
  138. package/lib/ui.js.map +1 -1
  139. package/package.json +2 -1
  140. package/src/citation/core/index.ts +1 -0
  141. package/src/citation/core/platformPaper.ts +32 -0
  142. package/src/color/core/index.ts +6 -1
  143. package/src/core.ts +1 -0
  144. package/src/ecosystem/core/sites.ts +16 -5
  145. package/src/ecosystem/ui/glyphs.tsx +19 -0
  146. package/src/orbital/core/index.ts +1 -1
  147. package/src/orbital/ui/AtomicOrbitalCanvas.tsx +9 -2
  148. package/src/orbital/ui/AtomicOrbitalViewer.tsx +5 -0
  149. package/src/orbital/ui/axesGeometry.ts +91 -0
  150. package/src/orbital/ui/camera.ts +9 -1
  151. package/src/orbital/ui/renderAxes.ts +190 -0
  152. package/src/orbital/ui/viewer.ts +32 -2
  153. package/src/periodic/core/categories.ts +83 -0
  154. package/src/periodic/core/elements.ts +217 -0
  155. package/src/periodic/core/index.ts +27 -0
  156. package/src/periodic/core/layout.ts +183 -0
  157. package/src/periodic/ui/CategoryLegend.tsx +105 -0
  158. package/src/periodic/ui/ElementCell.tsx +137 -0
  159. package/src/periodic/ui/PeriodicTable.tsx +226 -0
  160. package/src/periodic/ui/PeriodicTableChrome.tsx +170 -0
  161. package/src/periodic/ui/index.ts +6 -0
  162. package/src/seo/core/documentMeta.ts +5 -5
  163. package/src/seo/core/index.ts +19 -12
  164. package/src/seo/core/noscript.ts +195 -0
  165. package/src/seo/core/pageMeta.ts +54 -53
  166. package/src/seo/core/robots.ts +114 -0
  167. package/src/seo/core/routes.ts +181 -14
  168. package/src/seo/core/siteFiles.ts +58 -96
  169. package/src/seo/core/startDocumentMeta.ts +77 -0
  170. package/src/seo/core/structuredData.ts +80 -0
  171. package/src/seo/core/template.ts +54 -0
  172. package/src/seo/vite/ogCard.ts +15 -5
  173. package/src/seo/vite/prerender.ts +105 -58
  174. package/src/ui.ts +1 -0
@@ -15,9 +15,33 @@ export interface RouteMeta {
15
15
  title: string;
16
16
  /** One sentence, in the words someone would search for. */
17
17
  description: string;
18
+ /**
19
+ * The label the page is linked under where a title written for a search
20
+ * result is too long to read as a menu entry — the `noscript` index.
21
+ * @default the route's own title
22
+ */
23
+ short?: string;
24
+ /**
25
+ * What the page is for, written after an em dash next to its link in the
26
+ * `noscript` index.
27
+ * @default undefined — the link stands on its own
28
+ */
29
+ note?: string;
30
+ /**
31
+ * Whether the route also answers every address beneath it, so a section
32
+ * carrying more pages than a table can hold — an entry per structure, per
33
+ * ligand, per identifier — is indexed under the section rather than under the
34
+ * home page. Those addresses are canonical to the section itself.
35
+ * @default false
36
+ */
37
+ prefix?: boolean;
18
38
  }
19
39
  /**
20
40
  * The route an address names.
41
+ *
42
+ * An address a route claims exactly always wins over one that claims it as a
43
+ * subtree, and between two subtrees the longer claim wins, so `/molecules/HEM`
44
+ * is a molecule rather than whatever `/` answers.
21
45
  * @param routes - Every address the site answers.
22
46
  * @param path - Absolute path, without a query string.
23
47
  * @returns Its entry, or `undefined` when the site does not know the address.
@@ -29,13 +53,37 @@ export declare function routeFor(routes: readonly RouteMeta[], path: string): Ro
29
53
  * An address the site does not know is described as the home page rather than
30
54
  * invented on the fly, which is what the router does with it too. The query
31
55
  * string never reaches the answer: the structure being drawn and the
32
- * configuration a shared link carries are not pages of their own.
56
+ * configuration a shared link carries are not pages of their own. An absolute
57
+ * address is read for its path, so an app handing over `location.href` after an
58
+ * in-app move is answered rather than silently described as the home page.
59
+ *
60
+ * The route table is written from the site's own root, and a server behind a
61
+ * mount is handed the address the browser asked for — `/surge/exercises` for a
62
+ * table that names `/exercises`. So the address is read at the site's own root
63
+ * first, and the four lookups run in this order:
64
+ *
65
+ * 1. the mount taken off, claimed exactly;
66
+ * 2. the address as written, claimed exactly;
67
+ * 3. the mount taken off, claimed as a subtree;
68
+ * 4. the address as written, claimed as a subtree.
69
+ *
70
+ * Exact before subtree, or a `prefix` route — a home page answering everything
71
+ * beneath it above all — would claim every mounted address and the mount would
72
+ * never come off. Stripped before as-written, or the mount itself would open
73
+ * whichever page happens to carry the mount's own name rather than the site's
74
+ * front page. Taking the address as written second is what leaves an unmounted
75
+ * caller answering exactly as before, and lets a table whose own paths start
76
+ * with the mount's name still be read.
33
77
  * @param routes - Every address the site answers.
34
- * @param url - The address, query string and fragment included.
78
+ * @param url - The address, query string and fragment included, either as a
79
+ * path or as an absolute `scheme://host/path` address.
80
+ * @param basePath - The path the site is mounted at, when the address carries
81
+ * it, written `surge`, `/surge` or `/surge/`.
82
+ * @default '' — the address is already written from the site's own root
35
83
  * @returns The route it is indexed as.
36
84
  * @throws {Error} When the table is empty, so there is no page to fall back to.
37
85
  */
38
- export declare function pageMetaFor(routes: readonly RouteMeta[], url: string): RouteMeta;
86
+ export declare function pageMetaFor(routes: readonly RouteMeta[], url: string, basePath?: string): RouteMeta;
39
87
  /**
40
88
  * The page an unknown address falls back to.
41
89
  * @param routes - Every address the site answers.
@@ -44,9 +92,29 @@ export declare function pageMetaFor(routes: readonly RouteMeta[], url: string):
44
92
  */
45
93
  export declare function homeRoute(routes: readonly RouteMeta[]): RouteMeta;
46
94
  /**
47
- * Drop a trailing slash, so `/about/` and `/about` are one page.
95
+ * Check a route table before a build reads it as a set of file names.
96
+ *
97
+ * An address written twice ships two sitemap entries and two links to a page
98
+ * only the first entry describes, and one carrying a `..` segment writes its
99
+ * file outside the build output — a real build asked for `/../escaped` and got
100
+ * a sibling of `dist`. Two addresses that differ only in an empty segment or in
101
+ * case are the same defect wearing a disguise: `//x` and `/x` both write
102
+ * `dist/x/index.html`, and so do `/About` and `/about` on the case-insensitive
103
+ * filesystem macOS and Windows ship by default — one file, two sitemap entries,
104
+ * and only one of the two descriptions survives. All of it is author
105
+ * configuration read at build time, so it is refused where it is written rather
106
+ * than repaired where it lands.
107
+ * @param routes - Every address the site answers.
108
+ * @throws {Error} When the table is empty, names one address twice — under any
109
+ * of those spellings — or carries a path that is not one.
110
+ */
111
+ export declare function assertRoutes(routes: readonly RouteMeta[]): void;
112
+ /**
113
+ * Drop the trailing slashes, so `/about/` and `/about` are one page and an
114
+ * origin written `https://host/surge//` composes one address rather than one
115
+ * with an empty segment in it.
48
116
  * @param value - A path or an origin.
49
- * @returns It, without the trailing slash `/` itself keeps.
117
+ * @returns It, without the trailing slashes `/` itself keeps.
50
118
  */
51
119
  export declare function trimTrailingSlash(value: string): string;
52
120
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/seo/core/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,SAAS,CAMvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,GAAG,EAAE,MAAM,GACV,SAAS,CAKX;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,CAIjE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/seo/core/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,SAAS,CAEvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,GAAG,EAAE,MAAM,EACX,QAAQ,SAAK,GACZ,SAAS,CAWX;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,CAIjE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAwB/D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGvD"}
@@ -7,19 +7,25 @@
7
7
  * running app, which retitles the tab after an in-app move. A page missing from
8
8
  * the table is a page a search engine only ever sees as the home page.
9
9
  */
10
+ import { stripBasePath } from "../../router/core/basePath.js";
11
+ const QUERY_OR_FRAGMENT = /[?#]/;
12
+ const TRAILING_SLASHES = /\/+$/;
13
+ // A scheme and an authority: what `location.href` hands out, and the one shape
14
+ // that cannot be confused with a path. `//host/path` is left as a path, because
15
+ // a route table is free to name one.
16
+ const ABSOLUTE_URL = /^[a-z][\d+.a-z-]*:\/\//i;
10
17
  /**
11
18
  * The route an address names.
19
+ *
20
+ * An address a route claims exactly always wins over one that claims it as a
21
+ * subtree, and between two subtrees the longer claim wins, so `/molecules/HEM`
22
+ * is a molecule rather than whatever `/` answers.
12
23
  * @param routes - Every address the site answers.
13
24
  * @param path - Absolute path, without a query string.
14
25
  * @returns Its entry, or `undefined` when the site does not know the address.
15
26
  */
16
27
  export function routeFor(routes, path) {
17
- const wanted = trimTrailingSlash(path) || '/';
18
- for (const route of routes) {
19
- if (trimTrailingSlash(route.path) === wanted)
20
- return route;
21
- }
22
- return undefined;
28
+ return exactRoute(routes, path) ?? prefixRoute(routes, path);
23
29
  }
24
30
  /**
25
31
  * The page an address opens.
@@ -27,17 +33,45 @@ export function routeFor(routes, path) {
27
33
  * An address the site does not know is described as the home page rather than
28
34
  * invented on the fly, which is what the router does with it too. The query
29
35
  * string never reaches the answer: the structure being drawn and the
30
- * configuration a shared link carries are not pages of their own.
36
+ * configuration a shared link carries are not pages of their own. An absolute
37
+ * address is read for its path, so an app handing over `location.href` after an
38
+ * in-app move is answered rather than silently described as the home page.
39
+ *
40
+ * The route table is written from the site's own root, and a server behind a
41
+ * mount is handed the address the browser asked for — `/surge/exercises` for a
42
+ * table that names `/exercises`. So the address is read at the site's own root
43
+ * first, and the four lookups run in this order:
44
+ *
45
+ * 1. the mount taken off, claimed exactly;
46
+ * 2. the address as written, claimed exactly;
47
+ * 3. the mount taken off, claimed as a subtree;
48
+ * 4. the address as written, claimed as a subtree.
49
+ *
50
+ * Exact before subtree, or a `prefix` route — a home page answering everything
51
+ * beneath it above all — would claim every mounted address and the mount would
52
+ * never come off. Stripped before as-written, or the mount itself would open
53
+ * whichever page happens to carry the mount's own name rather than the site's
54
+ * front page. Taking the address as written second is what leaves an unmounted
55
+ * caller answering exactly as before, and lets a table whose own paths start
56
+ * with the mount's name still be read.
31
57
  * @param routes - Every address the site answers.
32
- * @param url - The address, query string and fragment included.
58
+ * @param url - The address, query string and fragment included, either as a
59
+ * path or as an absolute `scheme://host/path` address.
60
+ * @param basePath - The path the site is mounted at, when the address carries
61
+ * it, written `surge`, `/surge` or `/surge/`.
62
+ * @default '' — the address is already written from the site's own root
33
63
  * @returns The route it is indexed as.
34
64
  * @throws {Error} When the table is empty, so there is no page to fall back to.
35
65
  */
36
- export function pageMetaFor(routes, url) {
66
+ export function pageMetaFor(routes, url, basePath = '') {
37
67
  const home = homeRoute(routes);
38
- const cut = url.search(/[?#]/);
39
- const path = cut === -1 ? url : url.slice(0, cut);
40
- return routeFor(routes, path) ?? home;
68
+ const path = pathOf(url);
69
+ const own = stripBasePath(basePath, path);
70
+ return (exactRoute(routes, own) ??
71
+ exactRoute(routes, path) ??
72
+ prefixRoute(routes, own) ??
73
+ prefixRoute(routes, path) ??
74
+ home);
41
75
  }
42
76
  /**
43
77
  * The page an unknown address falls back to.
@@ -49,14 +83,106 @@ export function homeRoute(routes) {
49
83
  const first = routes[0];
50
84
  if (first === undefined)
51
85
  throw new Error('a site answers at least one route');
52
- return routeFor(routes, '/') ?? first;
86
+ return exactRoute(routes, '/') ?? first;
87
+ }
88
+ /**
89
+ * Check a route table before a build reads it as a set of file names.
90
+ *
91
+ * An address written twice ships two sitemap entries and two links to a page
92
+ * only the first entry describes, and one carrying a `..` segment writes its
93
+ * file outside the build output — a real build asked for `/../escaped` and got
94
+ * a sibling of `dist`. Two addresses that differ only in an empty segment or in
95
+ * case are the same defect wearing a disguise: `//x` and `/x` both write
96
+ * `dist/x/index.html`, and so do `/About` and `/about` on the case-insensitive
97
+ * filesystem macOS and Windows ship by default — one file, two sitemap entries,
98
+ * and only one of the two descriptions survives. All of it is author
99
+ * configuration read at build time, so it is refused where it is written rather
100
+ * than repaired where it lands.
101
+ * @param routes - Every address the site answers.
102
+ * @throws {Error} When the table is empty, names one address twice — under any
103
+ * of those spellings — or carries a path that is not one.
104
+ */
105
+ export function assertRoutes(routes) {
106
+ if (routes.length === 0)
107
+ throw new Error('a site answers at least one route');
108
+ const claimed = new Set();
109
+ const folded = new Map();
110
+ for (const route of routes) {
111
+ const written = JSON.stringify(route.path);
112
+ assertPath(route.path, written);
113
+ const address = trimTrailingSlash(route.path) || '/';
114
+ if (address.includes('//')) {
115
+ throw new Error(`a route path names no empty segment: ${written}`);
116
+ }
117
+ if (claimed.has(address)) {
118
+ throw new Error(`a route path is written once: ${written}`);
119
+ }
120
+ const first = folded.get(address.toLowerCase());
121
+ if (first !== undefined) {
122
+ throw new Error(`two route paths name one file on a case-insensitive disk: ${first} and ${written}`);
123
+ }
124
+ claimed.add(address);
125
+ folded.set(address.toLowerCase(), written);
126
+ }
53
127
  }
54
128
  /**
55
- * Drop a trailing slash, so `/about/` and `/about` are one page.
129
+ * Drop the trailing slashes, so `/about/` and `/about` are one page and an
130
+ * origin written `https://host/surge//` composes one address rather than one
131
+ * with an empty segment in it.
56
132
  * @param value - A path or an origin.
57
- * @returns It, without the trailing slash `/` itself keeps.
133
+ * @returns It, without the trailing slashes `/` itself keeps.
58
134
  */
59
135
  export function trimTrailingSlash(value) {
60
- return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
136
+ const trimmed = value.replace(TRAILING_SLASHES, '');
137
+ return trimmed === '' && value !== '' ? '/' : trimmed;
138
+ }
139
+ function assertPath(path, written) {
140
+ if (!path.startsWith('/')) {
141
+ throw new Error(`a route path starts at the site root: ${written}`);
142
+ }
143
+ if (QUERY_OR_FRAGMENT.test(path)) {
144
+ throw new Error(`a route path carries no query string and no fragment: ${written}`);
145
+ }
146
+ if (path.split('/').includes('..')) {
147
+ throw new Error(`a route path stays inside the site: ${written}`);
148
+ }
149
+ }
150
+ // The path half of whatever the caller had at hand: an absolute address, or a
151
+ // path already, with the query string and the fragment cut off either way.
152
+ function pathOf(url) {
153
+ if (ABSOLUTE_URL.test(url) && URL.canParse(url))
154
+ return new URL(url).pathname;
155
+ const cut = url.search(QUERY_OR_FRAGMENT);
156
+ return cut === -1 ? url : url.slice(0, cut);
157
+ }
158
+ function exactRoute(routes, path) {
159
+ const wanted = trimTrailingSlash(path) || '/';
160
+ for (const route of routes) {
161
+ if ((trimTrailingSlash(route.path) || '/') === wanted)
162
+ return route;
163
+ }
164
+ return undefined;
165
+ }
166
+ function prefixRoute(routes, path) {
167
+ const wanted = trimTrailingSlash(path) || '/';
168
+ let claimed;
169
+ let claimedLength = -1;
170
+ for (const route of routes) {
171
+ if (route.prefix !== true)
172
+ continue;
173
+ const routePath = trimTrailingSlash(route.path) || '/';
174
+ if (!isUnder(routePath, wanted))
175
+ continue;
176
+ if (routePath.length > claimedLength) {
177
+ claimed = route;
178
+ claimedLength = routePath.length;
179
+ }
180
+ }
181
+ return claimed;
182
+ }
183
+ function isUnder(routePath, path) {
184
+ // `/surgeon` is not a page of `/surge`, so a claim only holds when what
185
+ // follows it is a path of its own.
186
+ return routePath === '/' || path.startsWith(`${routePath}/`);
61
187
  }
62
188
  //# sourceMappingURL=routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/seo/core/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,MAA4B,EAC5B,IAAY;IAEZ,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;IAC9C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;IAC7D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CACzB,MAA4B,EAC5B,GAAW;IAEX,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAClD,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,MAA4B;IACpD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAC9E,OAAO,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC9E,CAAC"}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/seo/core/routes.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  /**
2
- * The files and blocks a crawler reads besides the head: the sitemap, the
3
- * robots policy, the structured-data block and the list of addresses a visitor
4
- * without JavaScript can still follow.
2
+ * The sitemap, and what every other file a crawler fetches on its own is
3
+ * derived from: which site is being written, where it is served, and the path
4
+ * it is mounted at.
5
5
  *
6
- * All four are derived from the site's own record and its route table, so a
7
- * page added to the table is added to every one of them at once.
6
+ * A deployment names where it serves the site in full — origin and mount path
7
+ * in one value — because the origin is what a canonical link and a sitemap
8
+ * entry need. The mount is read back out of it here, so the addresses these
9
+ * files hand out start where the site actually answers.
8
10
  */
9
11
  import type { EcosystemSite, SiteId } from '../../ecosystem/core/sites.ts';
10
12
  import type { RouteMeta } from './routes.ts';
@@ -15,59 +17,53 @@ export interface SiteFilesOptions {
15
17
  /** Every address it answers. */
16
18
  routes: readonly RouteMeta[];
17
19
  /**
18
- * Origin every absolute address is built on.
20
+ * Where the site is served, mount path included, e.g.
21
+ * `https://learn.cheminfo.org/surge`. Every absolute address is built on it,
22
+ * and every path one of these files writes starts at its mount.
19
23
  * @default `https://<the site's host>`
20
24
  */
21
25
  origin?: string;
22
26
  }
23
27
  /**
24
28
  * Every routed address, as the sitemap lists them.
29
+ *
30
+ * A sitemap names at least one address: `<url>` is required by the sitemaps.org
31
+ * schema, and `robots.txt` advertises the file, so an empty one is reported as
32
+ * an error on every fetch rather than read as a site with nothing to index.
25
33
  * @param options - The site and its routes.
26
34
  * @returns The `sitemap.xml` document.
35
+ * @throws {Error} When the site answers no route, or names an origin that is
36
+ * not an absolute address.
27
37
  */
28
38
  export declare function sitemapXml(options: SiteFilesOptions): string;
29
39
  /**
30
- * The crawl policy.
31
- *
32
- * Our tools are meant to be found, so only the endpoints are disallowed — an
33
- * API prefix and its documentation are not pages. The sitemap is named only
34
- * because this module also writes it: a `Sitemap:` line pointing at a 404 is
35
- * reported as an error on every fetch.
36
- * @param options - The site and its routes.
37
- * @param disallow - Address prefixes to keep out of the index.
38
- * @returns The `robots.txt` document.
40
+ * The site these files are being written for.
41
+ * @param site - The site, named or passed.
42
+ * @returns Its record.
39
43
  */
40
- export declare function robotsTxt(options: SiteFilesOptions, disallow?: readonly string[]): string;
41
- /** What the structured-data block says the tool is. */
42
- export interface StructuredDataOptions extends SiteFilesOptions {
43
- /**
44
- * The schema.org application category.
45
- * @default 'EducationalApplication'
46
- */
47
- category?: string;
48
- /**
49
- * What the tool needs to run.
50
- * @default 'Any modern browser'
51
- */
52
- operatingSystem?: string;
53
- }
44
+ export declare function resolveSite(site: EcosystemSite | SiteId): EcosystemSite;
54
45
  /**
55
- * One `application/ld+json` block describing the tool.
46
+ * Where the site is served, as an absolute address without a trailing slash.
56
47
  *
57
- * It is the same on every page of a site — what varies per page is the head —
58
- * so it is written into the built page once rather than per route.
59
- * @param options - The site, and what kind of application it is.
60
- * @returns The script tag, ready to put in the head.
48
+ * It is an absolute `http` or `https` address or it is refused: a canonical
49
+ * link, an `og:url` and a sitemap entry are addresses a crawler resolves on its
50
+ * own, and one written from an origin missing its scheme is resolved against
51
+ * whatever directory the page was fetched from — pointing every page of the
52
+ * site at a sibling of itself. A dev or staging origin written `localhost:3000`
53
+ * is refused for the same reason: it parses, but as a path under a `localhost:`
54
+ * scheme, so the mount read back off it would be `/3000`.
55
+ * @param options - The site and where it is served.
56
+ * @returns The origin, mount path included when the deployment named one.
57
+ * @throws {Error} When the deployment named something that is not an absolute
58
+ * `http` or `https` address.
61
59
  */
62
- export declare function structuredDataScript(options: StructuredDataOptions): string;
60
+ export declare function originOf(options: SiteFilesOptions): string;
63
61
  /**
64
- * A readable page for a visitor, or a crawler, with no JavaScript.
65
- *
66
- * The body of our sites is an empty root element, so this is the only crawl
67
- * path through them that costs nothing to render — and it is honest: it says
68
- * the tool needs JavaScript, and links every address it answers.
69
- * @param options - The site and its routes.
70
- * @returns The `noscript` block, ready to put in the body.
62
+ * The path the deployment is mounted at, read off the address it named.
63
+ * @param options - The site and where it is served.
64
+ * @returns `''` for a site owning its host, `/surge` for one mounted under it.
65
+ * @throws {Error} When the deployment named something that is not an absolute
66
+ * address, so there is no path to read off it.
71
67
  */
72
- export declare function noscriptIndex(options: SiteFilesOptions): string;
68
+ export declare function mountPathOf(options: SiteFilesOptions): string;
73
69
  //# sourceMappingURL=siteFiles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"siteFiles.d.ts","sourceRoot":"","sources":["../../../src/seo/core/siteFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,IAAI,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7B,gCAAgC;IAChC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAa5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,gBAAgB,EACzB,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC/B,MAAM,CAKR;AAED,uDAAuD;AACvD,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAkB3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAe/D"}
1
+ {"version":3,"file":"siteFiles.d.ts","sourceRoot":"","sources":["../../../src/seo/core/siteFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAI3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQ7C,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,IAAI,EAAE,aAAa,GAAG,MAAM,CAAC;IAC7B,gCAAgC;IAChC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAgB5D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,aAAa,CAEvE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAQ1D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAE7D"}
@@ -1,23 +1,37 @@
1
1
  /**
2
- * The files and blocks a crawler reads besides the head: the sitemap, the
3
- * robots policy, the structured-data block and the list of addresses a visitor
4
- * without JavaScript can still follow.
2
+ * The sitemap, and what every other file a crawler fetches on its own is
3
+ * derived from: which site is being written, where it is served, and the path
4
+ * it is mounted at.
5
5
  *
6
- * All four are derived from the site's own record and its route table, so a
7
- * page added to the table is added to every one of them at once.
6
+ * A deployment names where it serves the site in full — origin and mount path
7
+ * in one value — because the origin is what a canonical link and a sitemap
8
+ * entry need. The mount is read back out of it here, so the addresses these
9
+ * files hand out start where the site actually answers.
8
10
  */
9
- import { siteById, siteDisplayName } from "../../ecosystem/core/lookup.js";
10
- import { escapeAttribute, escapeText } from "../../share/core/escape.js";
11
+ import { siteById } from "../../ecosystem/core/lookup.js";
12
+ import { basePathOf } from "../../router/core/basePath.js";
13
+ import { escapeText } from "../../share/core/escape.js";
11
14
  import { trimTrailingSlash } from "./routes.js";
12
- /** The sequence that must not appear raw inside a script element. */
13
- const SCRIPT_SAFE_LESS_THAN = String.raw `\u003c`;
15
+ // A crawler fetches what it is given over HTTP, so an origin is written in one
16
+ // of the two schemes it speaks. Parsing alone does not say that: `localhost:3000`
17
+ // parses, with `localhost:` as its scheme and `3000` as its path.
18
+ const HTTP_ORIGIN = /^https?:\/\//i;
14
19
  /**
15
20
  * Every routed address, as the sitemap lists them.
21
+ *
22
+ * A sitemap names at least one address: `<url>` is required by the sitemaps.org
23
+ * schema, and `robots.txt` advertises the file, so an empty one is reported as
24
+ * an error on every fetch rather than read as a site with nothing to index.
16
25
  * @param options - The site and its routes.
17
26
  * @returns The `sitemap.xml` document.
27
+ * @throws {Error} When the site answers no route, or names an origin that is
28
+ * not an absolute address.
18
29
  */
19
30
  export function sitemapXml(options) {
20
31
  const origin = originOf(options);
32
+ if (options.routes.length === 0) {
33
+ throw new Error('a sitemap lists at least one address');
34
+ }
21
35
  const entries = options.routes
22
36
  .map((route) => ` <url><loc>${escapeText(`${origin}${route.path}`)}</loc></url>`)
23
37
  .join('\n');
@@ -28,73 +42,43 @@ ${entries}
28
42
  `;
29
43
  }
30
44
  /**
31
- * The crawl policy.
32
- *
33
- * Our tools are meant to be found, so only the endpoints are disallowed — an
34
- * API prefix and its documentation are not pages. The sitemap is named only
35
- * because this module also writes it: a `Sitemap:` line pointing at a 404 is
36
- * reported as an error on every fetch.
37
- * @param options - The site and its routes.
38
- * @param disallow - Address prefixes to keep out of the index.
39
- * @returns The `robots.txt` document.
45
+ * The site these files are being written for.
46
+ * @param site - The site, named or passed.
47
+ * @returns Its record.
40
48
  */
41
- export function robotsTxt(options, disallow = []) {
42
- const lines = ['User-agent: *', 'Allow: /'];
43
- for (const path of disallow)
44
- lines.push(`Disallow: ${path}`);
45
- lines.push('', `Sitemap: ${originOf(options)}/sitemap.xml`, '');
46
- return lines.join('\n');
49
+ export function resolveSite(site) {
50
+ return typeof site === 'string' ? siteById(site) : site;
47
51
  }
48
52
  /**
49
- * One `application/ld+json` block describing the tool.
53
+ * Where the site is served, as an absolute address without a trailing slash.
50
54
  *
51
- * It is the same on every page of a site — what varies per page is the head —
52
- * so it is written into the built page once rather than per route.
53
- * @param options - The site, and what kind of application it is.
54
- * @returns The script tag, ready to put in the head.
55
+ * It is an absolute `http` or `https` address or it is refused: a canonical
56
+ * link, an `og:url` and a sitemap entry are addresses a crawler resolves on its
57
+ * own, and one written from an origin missing its scheme is resolved against
58
+ * whatever directory the page was fetched from — pointing every page of the
59
+ * site at a sibling of itself. A dev or staging origin written `localhost:3000`
60
+ * is refused for the same reason: it parses, but as a path under a `localhost:`
61
+ * scheme, so the mount read back off it would be `/3000`.
62
+ * @param options - The site and where it is served.
63
+ * @returns The origin, mount path included when the deployment named one.
64
+ * @throws {Error} When the deployment named something that is not an absolute
65
+ * `http` or `https` address.
55
66
  */
56
- export function structuredDataScript(options) {
57
- const site = resolveSite(options.site);
58
- const data = {
59
- '@context': 'https://schema.org',
60
- '@type': 'WebApplication',
61
- name: siteDisplayName(site),
62
- url: `${originOf(options)}/`,
63
- description: site.tagline,
64
- applicationCategory: options.category ?? 'EducationalApplication',
65
- operatingSystem: options.operatingSystem ?? 'Any modern browser',
66
- offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
67
- publisher: { '@type': 'Organization', name: 'cheminfo' },
68
- };
69
- const json = JSON.stringify(data, null, 2).replaceAll('<', SCRIPT_SAFE_LESS_THAN);
70
- return `<script type="application/ld+json">\n${json}\n</script>`;
67
+ export function originOf(options) {
68
+ const origin = options.origin ?? `https://${resolveSite(options.site).host}`;
69
+ if (!HTTP_ORIGIN.test(origin) || !URL.canParse(origin)) {
70
+ throw new Error(`an origin is an absolute address, e.g. https://surge.cheminfo.org: ${JSON.stringify(origin)}`);
71
+ }
72
+ return trimTrailingSlash(origin);
71
73
  }
72
74
  /**
73
- * A readable page for a visitor, or a crawler, with no JavaScript.
74
- *
75
- * The body of our sites is an empty root element, so this is the only crawl
76
- * path through them that costs nothing to render — and it is honest: it says
77
- * the tool needs JavaScript, and links every address it answers.
78
- * @param options - The site and its routes.
79
- * @returns The `noscript` block, ready to put in the body.
75
+ * The path the deployment is mounted at, read off the address it named.
76
+ * @param options - The site and where it is served.
77
+ * @returns `''` for a site owning its host, `/surge` for one mounted under it.
78
+ * @throws {Error} When the deployment named something that is not an absolute
79
+ * address, so there is no path to read off it.
80
80
  */
81
- export function noscriptIndex(options) {
82
- const site = resolveSite(options.site);
83
- const items = options.routes
84
- .map((route) => ` <li><a href="${escapeAttribute(route.path)}">${escapeText(route.title)}</a></li>`)
85
- .join('\n');
86
- return `<noscript>
87
- <h1>${escapeText(siteDisplayName(site))}</h1>
88
- <p>${escapeText(site.tagline)} This tool needs JavaScript; these are the pages it offers:</p>
89
- <ul>
90
- ${items}
91
- </ul>
92
- </noscript>`;
93
- }
94
- function resolveSite(site) {
95
- return typeof site === 'string' ? siteById(site) : site;
96
- }
97
- function originOf(options) {
98
- return trimTrailingSlash(options.origin ?? `https://${resolveSite(options.site).host}`);
81
+ export function mountPathOf(options) {
82
+ return basePathOf(originOf(options));
99
83
  }
100
84
  //# sourceMappingURL=siteFiles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"siteFiles.js","sourceRoot":"","sources":["../../../src/seo/core/siteFiles.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"siteFiles.js","sourceRoot":"","sources":["../../../src/seo/core/siteFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAGxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,+EAA+E;AAC/E,kFAAkF;AAClF,kEAAkE;AAClE,MAAM,WAAW,GAAG,eAAe,CAAC;AAiBpC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,OAAyB;IAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM;SAC3B,GAAG,CACF,CAAC,KAAK,EAAE,EAAE,CACR,eAAe,UAAU,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,cAAc,CACpE;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;;EAEP,OAAO;;CAER,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAA4B;IACtD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAyB;IAChD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7E,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CACb,sEAAsE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAyB;IACnD,OAAO,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AACvC,CAAC"}