react-cheminfo 0.64.1 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -0
- package/lib/card/core/cardAddress.d.ts +70 -0
- package/lib/card/core/cardAddress.d.ts.map +1 -0
- package/lib/card/core/cardAddress.js +59 -0
- package/lib/card/core/cardAddress.js.map +1 -0
- package/lib/card/core/cardPageUrl.d.ts +49 -0
- package/lib/card/core/cardPageUrl.d.ts.map +1 -0
- package/lib/card/core/cardPageUrl.js +59 -0
- package/lib/card/core/cardPageUrl.js.map +1 -0
- package/lib/card/core/index.d.ts +7 -0
- package/lib/card/core/index.d.ts.map +1 -0
- package/lib/card/core/index.js +4 -0
- package/lib/card/core/index.js.map +1 -0
- package/lib/card/core/panelRoom.d.ts +40 -0
- package/lib/card/core/panelRoom.d.ts.map +1 -0
- package/lib/card/core/panelRoom.js +40 -0
- package/lib/card/core/panelRoom.js.map +1 -0
- package/lib/card/ui/CardDetailsButton.d.ts +38 -0
- package/lib/card/ui/CardDetailsButton.d.ts.map +1 -0
- package/lib/card/ui/CardDetailsButton.js +50 -0
- package/lib/card/ui/CardDetailsButton.js.map +1 -0
- package/lib/card/ui/CardFigure.d.ts +51 -0
- package/lib/card/ui/CardFigure.d.ts.map +1 -0
- package/lib/card/ui/CardFigure.js +27 -0
- package/lib/card/ui/CardFigure.js.map +1 -0
- package/lib/card/ui/CardFrame.d.ts +30 -0
- package/lib/card/ui/CardFrame.d.ts.map +1 -0
- package/lib/card/ui/CardFrame.js +20 -0
- package/lib/card/ui/CardFrame.js.map +1 -0
- package/lib/card/ui/CopyTableButton.d.ts +37 -0
- package/lib/card/ui/CopyTableButton.d.ts.map +1 -0
- package/lib/card/ui/CopyTableButton.js +19 -0
- package/lib/card/ui/CopyTableButton.js.map +1 -0
- package/lib/card/ui/cardTooltip.d.ts +14 -0
- package/lib/card/ui/cardTooltip.d.ts.map +1 -0
- package/lib/card/ui/cardTooltip.js +15 -0
- package/lib/card/ui/cardTooltip.js.map +1 -0
- package/lib/card/ui/embed.d.ts +15 -0
- package/lib/card/ui/embed.d.ts.map +1 -0
- package/lib/card/ui/embed.js +24 -0
- package/lib/card/ui/embed.js.map +1 -0
- package/lib/card/ui/fitCardToFrame.d.ts +31 -0
- package/lib/card/ui/fitCardToFrame.d.ts.map +1 -0
- package/lib/card/ui/fitCardToFrame.js +66 -0
- package/lib/card/ui/fitCardToFrame.js.map +1 -0
- package/lib/card/ui/holdRoomForPanels.d.ts +16 -0
- package/lib/card/ui/holdRoomForPanels.d.ts.map +1 -0
- package/lib/card/ui/holdRoomForPanels.js +76 -0
- package/lib/card/ui/holdRoomForPanels.js.map +1 -0
- package/lib/card/ui/index.d.ts +11 -0
- package/lib/card/ui/index.d.ts.map +1 -0
- package/lib/card/ui/index.js +7 -0
- package/lib/card/ui/index.js.map +1 -0
- package/lib/card/ui/openPanels.d.ts +25 -0
- package/lib/card/ui/openPanels.d.ts.map +1 -0
- package/lib/card/ui/openPanels.js +85 -0
- package/lib/card/ui/openPanels.js.map +1 -0
- package/lib/card.d.ts +13 -0
- package/lib/card.d.ts.map +1 -0
- package/lib/card.js +13 -0
- package/lib/card.js.map +1 -0
- package/lib/conformers.d.ts +1 -0
- package/lib/conformers.d.ts.map +1 -1
- package/lib/conformers.js +1 -0
- package/lib/conformers.js.map +1 -1
- package/lib/core.d.ts +4 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +5 -0
- package/lib/core.js.map +1 -1
- package/lib/fetch/core/apiError.d.ts +56 -0
- package/lib/fetch/core/apiError.d.ts.map +1 -0
- package/lib/fetch/core/apiError.js +43 -0
- package/lib/fetch/core/apiError.js.map +1 -0
- package/lib/fetch/core/fetchJson.d.ts +47 -0
- package/lib/fetch/core/fetchJson.d.ts.map +1 -0
- package/lib/fetch/core/fetchJson.js +109 -0
- package/lib/fetch/core/fetchJson.js.map +1 -0
- package/lib/fetch/core/index.d.ts +5 -0
- package/lib/fetch/core/index.d.ts.map +1 -0
- package/lib/fetch/core/index.js +3 -0
- package/lib/fetch/core/index.js.map +1 -0
- package/lib/formula/core/formulaText.d.ts +46 -0
- package/lib/formula/core/formulaText.d.ts.map +1 -0
- package/lib/formula/core/formulaText.js +74 -0
- package/lib/formula/core/formulaText.js.map +1 -0
- package/lib/formula/core/index.d.ts +6 -0
- package/lib/formula/core/index.d.ts.map +1 -0
- package/lib/formula/core/index.js +4 -0
- package/lib/formula/core/index.js.map +1 -0
- package/lib/formula/core/normalizeFormula.d.ts +22 -0
- package/lib/formula/core/normalizeFormula.d.ts.map +1 -0
- package/lib/formula/core/normalizeFormula.js +46 -0
- package/lib/formula/core/normalizeFormula.js.map +1 -0
- package/lib/formula/core/readFormula.d.ts +73 -0
- package/lib/formula/core/readFormula.d.ts.map +1 -0
- package/lib/formula/core/readFormula.js +132 -0
- package/lib/formula/core/readFormula.js.map +1 -0
- package/lib/formula/ui/CopyableFormula.d.ts +31 -0
- package/lib/formula/ui/CopyableFormula.d.ts.map +1 -0
- package/lib/formula/ui/CopyableFormula.js +19 -0
- package/lib/formula/ui/CopyableFormula.js.map +1 -0
- package/lib/formula/ui/FormulaInput.d.ts +29 -0
- package/lib/formula/ui/FormulaInput.d.ts.map +1 -0
- package/lib/formula/ui/FormulaInput.js +16 -0
- package/lib/formula/ui/FormulaInput.js.map +1 -0
- package/lib/formula/ui/QuestionFormula.d.ts +14 -0
- package/lib/formula/ui/QuestionFormula.d.ts.map +1 -0
- package/lib/formula/ui/QuestionFormula.js +13 -0
- package/lib/formula/ui/QuestionFormula.js.map +1 -0
- package/lib/formula/ui/index.d.ts +7 -0
- package/lib/formula/ui/index.d.ts.map +1 -0
- package/lib/formula/ui/index.js +4 -0
- package/lib/formula/ui/index.js.map +1 -0
- package/lib/formula.d.ts +11 -0
- package/lib/formula.d.ts.map +1 -0
- package/lib/formula.js +11 -0
- package/lib/formula.js.map +1 -0
- package/lib/hooks/ui/index.d.ts +1 -0
- package/lib/hooks/ui/index.d.ts.map +1 -1
- package/lib/hooks/ui/index.js.map +1 -1
- package/lib/hooks/ui/keyTargets.d.ts +4 -2
- package/lib/hooks/ui/keyTargets.d.ts.map +1 -1
- package/lib/hooks/ui/keyTargets.js +5 -3
- package/lib/hooks/ui/keyTargets.js.map +1 -1
- package/lib/hooks/ui/useAsync.d.ts +20 -5
- package/lib/hooks/ui/useAsync.d.ts.map +1 -1
- package/lib/hooks/ui/useAsync.js +29 -14
- package/lib/hooks/ui/useAsync.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/locales/de.json +37 -0
- package/lib/locales/en.json +37 -0
- package/lib/locales/es.json +37 -0
- package/lib/locales/fr.json +37 -0
- package/lib/locales/it.json +37 -0
- package/lib/moleculesClient/core/index.d.ts +5 -0
- package/lib/moleculesClient/core/index.d.ts.map +1 -0
- package/lib/moleculesClient/core/index.js +3 -0
- package/lib/moleculesClient/core/index.js.map +1 -0
- package/lib/moleculesClient/core/moleculesClient.d.ts +12 -0
- package/lib/moleculesClient/core/moleculesClient.d.ts.map +1 -0
- package/lib/moleculesClient/core/moleculesClient.js +105 -0
- package/lib/moleculesClient/core/moleculesClient.js.map +1 -0
- package/lib/moleculesClient/core/moleculesClientApi.d.ts +100 -0
- package/lib/moleculesClient/core/moleculesClientApi.d.ts.map +1 -0
- package/lib/moleculesClient/core/moleculesClientApi.js +20 -0
- package/lib/moleculesClient/core/moleculesClientApi.js.map +1 -0
- package/lib/moleculesClient/core/moleculesTypes.d.ts +154 -0
- package/lib/moleculesClient/core/moleculesTypes.d.ts.map +1 -0
- package/lib/moleculesClient/core/moleculesTypes.js +2 -0
- package/lib/moleculesClient/core/moleculesTypes.js.map +1 -0
- package/lib/moleculesClient/core/requestRetry.d.ts +50 -0
- package/lib/moleculesClient/core/requestRetry.d.ts.map +1 -0
- package/lib/moleculesClient/core/requestRetry.js +108 -0
- package/lib/moleculesClient/core/requestRetry.js.map +1 -0
- package/lib/molstar/core/camera.d.ts +2 -1
- package/lib/molstar/core/camera.d.ts.map +1 -1
- package/lib/molstar/core/camera.js +3 -2
- package/lib/molstar/core/camera.js.map +1 -1
- package/lib/molstar/core/framing.d.ts +102 -0
- package/lib/molstar/core/framing.d.ts.map +1 -0
- package/lib/molstar/core/framing.js +120 -0
- package/lib/molstar/core/framing.js.map +1 -0
- package/lib/molstar/core/hover.d.ts +16 -0
- package/lib/molstar/core/hover.d.ts.map +1 -1
- package/lib/molstar/core/hover.js +14 -0
- package/lib/molstar/core/hover.js.map +1 -1
- package/lib/molstar/core/index.d.ts +4 -1
- package/lib/molstar/core/index.d.ts.map +1 -1
- package/lib/molstar/core/index.js +2 -1
- package/lib/molstar/core/index.js.map +1 -1
- package/lib/pedagogy/core/answers.d.ts +46 -0
- package/lib/pedagogy/core/answers.d.ts.map +1 -0
- package/lib/pedagogy/core/answers.js +77 -0
- package/lib/pedagogy/core/answers.js.map +1 -0
- package/lib/pedagogy/core/attempts.d.ts +86 -0
- package/lib/pedagogy/core/attempts.d.ts.map +1 -0
- package/lib/pedagogy/core/attempts.js +117 -0
- package/lib/pedagogy/core/attempts.js.map +1 -0
- package/lib/pedagogy/core/index.d.ts +8 -0
- package/lib/pedagogy/core/index.d.ts.map +1 -1
- package/lib/pedagogy/core/index.js +4 -0
- package/lib/pedagogy/core/index.js.map +1 -1
- package/lib/pedagogy/core/progressController.d.ts +64 -0
- package/lib/pedagogy/core/progressController.d.ts.map +1 -0
- package/lib/pedagogy/core/progressController.js +56 -0
- package/lib/pedagogy/core/progressController.js.map +1 -0
- package/lib/pedagogy/core/seed.d.ts +96 -0
- package/lib/pedagogy/core/seed.d.ts.map +1 -0
- package/lib/pedagogy/core/seed.js +128 -0
- package/lib/pedagogy/core/seed.js.map +1 -0
- package/lib/pedagogy/core/series.d.ts +77 -0
- package/lib/pedagogy/core/series.d.ts.map +1 -0
- package/lib/pedagogy/core/series.js +10 -0
- package/lib/pedagogy/core/series.js.map +1 -0
- package/lib/pedagogy/ui/AnswerFields.d.ts +21 -0
- package/lib/pedagogy/ui/AnswerFields.d.ts.map +1 -0
- package/lib/pedagogy/ui/AnswerFields.js +24 -0
- package/lib/pedagogy/ui/AnswerFields.js.map +1 -0
- package/lib/pedagogy/ui/ExerciseList.d.ts +82 -0
- package/lib/pedagogy/ui/ExerciseList.d.ts.map +1 -0
- package/lib/pedagogy/ui/ExerciseList.js +53 -0
- package/lib/pedagogy/ui/ExerciseList.js.map +1 -0
- package/lib/pedagogy/ui/ExerciseSeries.d.ts +95 -0
- package/lib/pedagogy/ui/ExerciseSeries.d.ts.map +1 -0
- package/lib/pedagogy/ui/ExerciseSeries.js +74 -0
- package/lib/pedagogy/ui/ExerciseSeries.js.map +1 -0
- package/lib/pedagogy/ui/QuestionCard.d.ts +64 -0
- package/lib/pedagogy/ui/QuestionCard.d.ts.map +1 -0
- package/lib/pedagogy/ui/QuestionCard.js +73 -0
- package/lib/pedagogy/ui/QuestionCard.js.map +1 -0
- package/lib/pedagogy/ui/index.d.ts +10 -0
- package/lib/pedagogy/ui/index.d.ts.map +1 -1
- package/lib/pedagogy/ui/index.js +6 -0
- package/lib/pedagogy/ui/index.js.map +1 -1
- package/lib/pedagogy/ui/seriesCounts.d.ts +17 -0
- package/lib/pedagogy/ui/seriesCounts.d.ts.map +1 -0
- package/lib/pedagogy/ui/seriesCounts.js +27 -0
- package/lib/pedagogy/ui/seriesCounts.js.map +1 -0
- package/lib/pedagogy/ui/seriesParts.d.ts +55 -0
- package/lib/pedagogy/ui/seriesParts.d.ts.map +1 -0
- package/lib/pedagogy/ui/seriesParts.js +38 -0
- package/lib/pedagogy/ui/seriesParts.js.map +1 -0
- package/lib/pedagogy/ui/useAttemptRecords.d.ts +10 -0
- package/lib/pedagogy/ui/useAttemptRecords.d.ts.map +1 -0
- package/lib/pedagogy/ui/useAttemptRecords.js +11 -0
- package/lib/pedagogy/ui/useAttemptRecords.js.map +1 -0
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +6 -3
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/property/core/copyableText.d.ts +14 -0
- package/lib/property/core/copyableText.d.ts.map +1 -0
- package/lib/property/core/copyableText.js +21 -0
- package/lib/property/core/copyableText.js.map +1 -0
- package/lib/property/core/index.d.ts +2 -0
- package/lib/property/core/index.d.ts.map +1 -0
- package/lib/property/core/index.js +2 -0
- package/lib/property/core/index.js.map +1 -0
- package/lib/property/ui/PropertyTable.d.ts +43 -0
- package/lib/property/ui/PropertyTable.d.ts.map +1 -0
- package/lib/property/ui/PropertyTable.js +19 -0
- package/lib/property/ui/PropertyTable.js.map +1 -0
- package/lib/property/ui/index.d.ts +3 -0
- package/lib/property/ui/index.d.ts.map +1 -0
- package/lib/property/ui/index.js +2 -0
- package/lib/property/ui/index.js.map +1 -0
- package/lib/router/core/history.d.ts +24 -3
- package/lib/router/core/history.d.ts.map +1 -1
- package/lib/router/core/history.js +55 -7
- package/lib/router/core/history.js.map +1 -1
- package/lib/router/core/index.d.ts +3 -3
- package/lib/router/core/index.d.ts.map +1 -1
- package/lib/router/core/index.js +2 -2
- package/lib/router/core/index.js.map +1 -1
- package/lib/router/core/languageRoute.d.ts +61 -0
- package/lib/router/core/languageRoute.d.ts.map +1 -0
- package/lib/router/core/languageRoute.js +96 -0
- package/lib/router/core/languageRoute.js.map +1 -0
- package/lib/router/core/siteAddresses.d.ts +23 -0
- package/lib/router/core/siteAddresses.d.ts.map +1 -1
- package/lib/router/core/siteAddresses.js +26 -0
- package/lib/router/core/siteAddresses.js.map +1 -1
- package/lib/router/core/tabRouter.d.ts +2 -108
- package/lib/router/core/tabRouter.d.ts.map +1 -1
- package/lib/router/core/tabRouter.js +38 -5
- package/lib/router/core/tabRouter.js.map +1 -1
- package/lib/router/core/tabRouterTypes.d.ts +154 -0
- package/lib/router/core/tabRouterTypes.d.ts.map +1 -0
- package/lib/router/core/tabRouterTypes.js +2 -0
- package/lib/router/core/tabRouterTypes.js.map +1 -0
- package/lib/seo/core/fitDescription.d.ts +40 -0
- package/lib/seo/core/fitDescription.d.ts.map +1 -0
- package/lib/seo/core/fitDescription.js +45 -0
- package/lib/seo/core/fitDescription.js.map +1 -0
- package/lib/seo/core/index.d.ts +7 -0
- package/lib/seo/core/index.d.ts.map +1 -1
- package/lib/seo/core/index.js +4 -0
- package/lib/seo/core/index.js.map +1 -1
- package/lib/seo/core/limits.d.ts +24 -0
- package/lib/seo/core/limits.d.ts.map +1 -0
- package/lib/seo/core/limits.js +24 -0
- package/lib/seo/core/limits.js.map +1 -0
- package/lib/seo/core/pageContent.d.ts +25 -0
- package/lib/seo/core/pageContent.d.ts.map +1 -0
- package/lib/seo/core/pageContent.js +25 -0
- package/lib/seo/core/pageContent.js.map +1 -0
- package/lib/seo/core/plainDescription.d.ts.map +1 -1
- package/lib/seo/core/plainDescription.js +3 -2
- package/lib/seo/core/plainDescription.js.map +1 -1
- package/lib/seo/core/routeProblems.d.ts.map +1 -1
- package/lib/seo/core/routeProblems.js +1 -6
- package/lib/seo/core/routeProblems.js.map +1 -1
- package/lib/seo/core/routeTable.d.ts +31 -0
- package/lib/seo/core/routeTable.d.ts.map +1 -0
- package/lib/seo/core/routeTable.js +96 -0
- package/lib/seo/core/routeTable.js.map +1 -0
- package/lib/seo/vite/index.d.ts +2 -0
- package/lib/seo/vite/index.d.ts.map +1 -1
- package/lib/seo/vite/index.js +1 -0
- package/lib/seo/vite/index.js.map +1 -1
- package/lib/seo/vite/prerender.d.ts +10 -0
- package/lib/seo/vite/prerender.d.ts.map +1 -1
- package/lib/seo/vite/prerender.js +12 -1
- package/lib/seo/vite/prerender.js.map +1 -1
- package/lib/seo/vite/routeTable.d.ts +40 -0
- package/lib/seo/vite/routeTable.d.ts.map +1 -0
- package/lib/seo/vite/routeTable.js +44 -0
- package/lib/seo/vite/routeTable.js.map +1 -0
- package/lib/server/answerAddresses.d.ts +51 -0
- package/lib/server/answerAddresses.d.ts.map +1 -0
- package/lib/server/answerAddresses.js +72 -0
- package/lib/server/answerAddresses.js.map +1 -0
- package/lib/server/cardMeta.d.ts +57 -0
- package/lib/server/cardMeta.d.ts.map +1 -0
- package/lib/server/cardMeta.js +52 -0
- package/lib/server/cardMeta.js.map +1 -0
- package/lib/server/config.d.ts +72 -0
- package/lib/server/config.d.ts.map +1 -0
- package/lib/server/config.js +67 -0
- package/lib/server/config.js.map +1 -0
- package/lib/server/envNumber.d.ts +35 -0
- package/lib/server/envNumber.d.ts.map +1 -0
- package/lib/server/envNumber.js +42 -0
- package/lib/server/envNumber.js.map +1 -0
- package/lib/server/healthRoute.d.ts +26 -0
- package/lib/server/healthRoute.d.ts.map +1 -0
- package/lib/server/healthRoute.js +22 -0
- package/lib/server/healthRoute.js.map +1 -0
- package/lib/server/listenSite.d.ts +38 -0
- package/lib/server/listenSite.d.ts.map +1 -0
- package/lib/server/listenSite.js +60 -0
- package/lib/server/listenSite.js.map +1 -0
- package/lib/server/mcpServer.d.ts +8 -0
- package/lib/server/mcpServer.d.ts.map +1 -0
- package/lib/server/mcpServer.js +11 -0
- package/lib/server/mcpServer.js.map +1 -0
- package/lib/server/repositoryVersion.d.ts +16 -0
- package/lib/server/repositoryVersion.d.ts.map +1 -0
- package/lib/server/repositoryVersion.js +33 -0
- package/lib/server/repositoryVersion.js.map +1 -0
- package/lib/server/routeOptions.d.ts +23 -0
- package/lib/server/routeOptions.d.ts.map +1 -0
- package/lib/server/routeOptions.js +20 -0
- package/lib/server/routeOptions.js.map +1 -0
- package/lib/server/routeTable.d.ts +14 -0
- package/lib/server/routeTable.d.ts.map +1 -0
- package/lib/server/routeTable.js +32 -0
- package/lib/server/routeTable.js.map +1 -0
- package/lib/server/siteView.d.ts +29 -0
- package/lib/server/siteView.d.ts.map +1 -0
- package/lib/server/siteView.js +21 -0
- package/lib/server/siteView.js.map +1 -0
- package/lib/server/spaPages.d.ts +32 -0
- package/lib/server/spaPages.d.ts.map +1 -0
- package/lib/server/spaPages.js +126 -0
- package/lib/server/spaPages.js.map +1 -0
- package/lib/server/spaSite.d.ts +30 -0
- package/lib/server/spaSite.d.ts.map +1 -0
- package/lib/server/spaSite.js +170 -0
- package/lib/server/spaSite.js.map +1 -0
- package/lib/server/spaSiteOptions.d.ts +126 -0
- package/lib/server/spaSiteOptions.d.ts.map +1 -0
- package/lib/server/spaSiteOptions.js +2 -0
- package/lib/server/spaSiteOptions.js.map +1 -0
- package/lib/server/staticSite.d.ts +9 -0
- package/lib/server/staticSite.d.ts.map +1 -1
- package/lib/server/staticSite.js +41 -19
- package/lib/server/staticSite.js.map +1 -1
- package/lib/server/toolSiteServer.d.ts +77 -0
- package/lib/server/toolSiteServer.d.ts.map +1 -0
- package/lib/server/toolSiteServer.js +58 -0
- package/lib/server/toolSiteServer.js.map +1 -0
- package/lib/server/trustProxy.d.ts +20 -0
- package/lib/server/trustProxy.d.ts.map +1 -0
- package/lib/server/trustProxy.js +32 -0
- package/lib/server/trustProxy.js.map +1 -0
- package/lib/server/types.d.ts +13 -0
- package/lib/server/types.d.ts.map +1 -0
- package/lib/server/types.js +2 -0
- package/lib/server/types.js.map +1 -0
- package/lib/server.d.ts +22 -0
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +12 -0
- package/lib/server.js.map +1 -1
- package/lib/share/core/config.d.ts +18 -0
- package/lib/share/core/config.d.ts.map +1 -1
- package/lib/share/core/config.js +33 -2
- package/lib/share/core/config.js.map +1 -1
- package/lib/share/core/index.d.ts +3 -1
- package/lib/share/core/index.d.ts.map +1 -1
- package/lib/share/core/index.js +2 -1
- package/lib/share/core/index.js.map +1 -1
- package/lib/share/core/keySetParam.d.ts +40 -0
- package/lib/share/core/keySetParam.d.ts.map +1 -0
- package/lib/share/core/keySetParam.js +60 -0
- package/lib/share/core/keySetParam.js.map +1 -0
- package/lib/share/core/vocabulary.d.ts +8 -0
- package/lib/share/core/vocabulary.d.ts.map +1 -1
- package/lib/shell/ui/ShareControl.d.ts +75 -0
- package/lib/shell/ui/ShareControl.d.ts.map +1 -0
- package/lib/shell/ui/ShareControl.js +40 -0
- package/lib/shell/ui/ShareControl.js.map +1 -0
- package/lib/shell/ui/ShellFrame.d.ts +14 -0
- package/lib/shell/ui/ShellFrame.d.ts.map +1 -0
- package/lib/shell/ui/ShellFrame.js +45 -0
- package/lib/shell/ui/ShellFrame.js.map +1 -0
- package/lib/shell/ui/SiteShell.d.ts +251 -0
- package/lib/shell/ui/SiteShell.d.ts.map +1 -0
- package/lib/shell/ui/SiteShell.js +39 -0
- package/lib/shell/ui/SiteShell.js.map +1 -0
- package/lib/shell/ui/index.d.ts +5 -0
- package/lib/shell/ui/index.d.ts.map +1 -0
- package/lib/shell/ui/index.js +3 -0
- package/lib/shell/ui/index.js.map +1 -0
- package/lib/state/core/index.d.ts +1 -0
- package/lib/state/core/index.d.ts.map +1 -1
- package/lib/state/core/index.js.map +1 -1
- package/lib/state/core/legacyEntries.d.ts +30 -0
- package/lib/state/core/legacyEntries.d.ts.map +1 -0
- package/lib/state/core/legacyEntries.js +51 -0
- package/lib/state/core/legacyEntries.js.map +1 -0
- package/lib/state/core/persistBucket.d.ts +26 -2
- package/lib/state/core/persistBucket.d.ts.map +1 -1
- package/lib/state/core/persistBucket.js +26 -9
- package/lib/state/core/persistBucket.js.map +1 -1
- package/lib/state/core/persistSignalBucket.d.ts +26 -0
- package/lib/state/core/persistSignalBucket.d.ts.map +1 -1
- package/lib/state/core/persistSignalBucket.js +31 -0
- package/lib/state/core/persistSignalBucket.js.map +1 -1
- package/lib/state/core/storageEntry.d.ts +15 -0
- package/lib/state/core/storageEntry.d.ts.map +1 -1
- package/lib/state/core/storageEntry.js +33 -0
- package/lib/state/core/storageEntry.js.map +1 -1
- package/lib/structure/ui/StructureExportButton.d.ts +27 -0
- package/lib/structure/ui/StructureExportButton.d.ts.map +1 -0
- package/lib/structure/ui/StructureExportButton.js +21 -0
- package/lib/structure/ui/StructureExportButton.js.map +1 -0
- package/lib/structure/ui/index.d.ts +4 -0
- package/lib/structure/ui/index.d.ts.map +1 -1
- package/lib/structure/ui/index.js +2 -0
- package/lib/structure/ui/index.js.map +1 -1
- package/lib/structure/ui/useStructureDraft.d.ts +32 -0
- package/lib/structure/ui/useStructureDraft.d.ts.map +1 -0
- package/lib/structure/ui/useStructureDraft.js +20 -0
- package/lib/structure/ui/useStructureDraft.js.map +1 -0
- package/lib/ui.d.ts +2 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +2 -0
- package/lib/ui.js.map +1 -1
- package/lib/worker/core/index.d.ts +1 -1
- package/lib/worker/core/index.d.ts.map +1 -1
- package/lib/worker/core/messages.d.ts +4 -2
- package/lib/worker/core/messages.d.ts.map +1 -1
- package/lib/worker/core/messages.js.map +1 -1
- package/lib/worker/core/oclResourcesUrl.d.ts +16 -0
- package/lib/worker/core/oclResourcesUrl.d.ts.map +1 -0
- package/lib/worker/core/oclResourcesUrl.js +19 -0
- package/lib/worker/core/oclResourcesUrl.js.map +1 -0
- package/lib/worker/core/serveWorkerRequests.d.ts +8 -0
- package/lib/worker/core/serveWorkerRequests.d.ts.map +1 -1
- package/lib/worker/core/serveWorkerRequests.js +15 -2
- package/lib/worker/core/serveWorkerRequests.js.map +1 -1
- package/lib/worker/core/workerChannel.d.ts +3 -4
- package/lib/worker/core/workerChannel.d.ts.map +1 -1
- package/lib/worker/core/workerChannel.js +72 -67
- package/lib/worker/core/workerChannel.js.map +1 -1
- package/lib/worker/core/workerChannelParts.d.ts +17 -6
- package/lib/worker/core/workerChannelParts.d.ts.map +1 -1
- package/lib/worker/core/workerChannelParts.js +20 -4
- package/lib/worker/core/workerChannelParts.js.map +1 -1
- package/lib/worker/core/workerChannelTypes.d.ts +46 -2
- package/lib/worker/core/workerChannelTypes.d.ts.map +1 -1
- package/lib/worker/core/workerSlot.d.ts +66 -0
- package/lib/worker/core/workerSlot.d.ts.map +1 -0
- package/lib/worker/core/workerSlot.js +107 -0
- package/lib/worker/core/workerSlot.js.map +1 -0
- package/package.json +16 -1
- package/src/card/core/cardAddress.ts +100 -0
- package/src/card/core/cardPageUrl.ts +97 -0
- package/src/card/core/index.ts +6 -0
- package/src/card/core/panelRoom.ts +66 -0
- package/src/card/ui/CardDetailsButton.tsx +104 -0
- package/src/card/ui/CardFigure.tsx +86 -0
- package/src/card/ui/CardFrame.tsx +46 -0
- package/src/card/ui/CopyTableButton.tsx +81 -0
- package/src/card/ui/cardTooltip.ts +18 -0
- package/src/card/ui/embed.ts +27 -0
- package/src/card/ui/fitCardToFrame.ts +76 -0
- package/src/card/ui/holdRoomForPanels.ts +83 -0
- package/src/card/ui/index.ts +14 -0
- package/src/card/ui/openPanels.ts +106 -0
- package/src/card.ts +13 -0
- package/src/conformers.ts +4 -0
- package/src/core.ts +5 -0
- package/src/fetch/core/apiError.ts +65 -0
- package/src/fetch/core/fetchJson.ts +129 -0
- package/src/fetch/core/index.ts +4 -0
- package/src/formula/core/formulaText.ts +96 -0
- package/src/formula/core/index.ts +20 -0
- package/src/formula/core/normalizeFormula.ts +50 -0
- package/src/formula/core/readFormula.ts +211 -0
- package/src/formula/ui/CopyableFormula.tsx +51 -0
- package/src/formula/ui/FormulaInput.tsx +50 -0
- package/src/formula/ui/QuestionFormula.tsx +20 -0
- package/src/formula/ui/index.ts +6 -0
- package/src/formula.ts +11 -0
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/keyTargets.ts +5 -3
- package/src/hooks/ui/useAsync.ts +50 -14
- package/src/locales/de.json +37 -0
- package/src/locales/en.json +37 -0
- package/src/locales/es.json +37 -0
- package/src/locales/fr.json +37 -0
- package/src/locales/it.json +37 -0
- package/src/moleculesClient/core/index.ts +19 -0
- package/src/moleculesClient/core/moleculesClient.ts +175 -0
- package/src/moleculesClient/core/moleculesClientApi.ts +120 -0
- package/src/moleculesClient/core/moleculesTypes.ts +163 -0
- package/src/moleculesClient/core/requestRetry.ts +118 -0
- package/src/molstar/core/camera.ts +3 -2
- package/src/molstar/core/framing.ts +200 -0
- package/src/molstar/core/hover.ts +26 -0
- package/src/molstar/core/index.ts +21 -1
- package/src/pedagogy/core/answers.ts +94 -0
- package/src/pedagogy/core/attempts.ts +205 -0
- package/src/pedagogy/core/index.ts +36 -0
- package/src/pedagogy/core/progressController.ts +116 -0
- package/src/pedagogy/core/seed.ts +177 -0
- package/src/pedagogy/core/series.ts +82 -0
- package/src/pedagogy/ui/AnswerFields.tsx +63 -0
- package/src/pedagogy/ui/ExerciseList.tsx +202 -0
- package/src/pedagogy/ui/ExerciseSeries.tsx +250 -0
- package/src/pedagogy/ui/QuestionCard.tsx +231 -0
- package/src/pedagogy/ui/index.ts +18 -0
- package/src/pedagogy/ui/seriesCounts.ts +34 -0
- package/src/pedagogy/ui/seriesParts.tsx +127 -0
- package/src/pedagogy/ui/useAttemptRecords.ts +16 -0
- package/src/periodic/ui/PeriodicTable.tsx +6 -3
- package/src/property/core/copyableText.ts +20 -0
- package/src/property/core/index.ts +1 -0
- package/src/property/ui/PropertyTable.tsx +83 -0
- package/src/property/ui/index.ts +2 -0
- package/src/router/core/history.ts +61 -7
- package/src/router/core/index.ts +3 -2
- package/src/router/core/languageRoute.ts +136 -0
- package/src/router/core/siteAddresses.ts +38 -0
- package/src/router/core/tabRouter.ts +59 -117
- package/src/router/core/tabRouterTypes.ts +158 -0
- package/src/seo/core/fitDescription.ts +74 -0
- package/src/seo/core/index.ts +17 -0
- package/src/seo/core/limits.ts +27 -0
- package/src/seo/core/pageContent.ts +42 -0
- package/src/seo/core/plainDescription.ts +4 -2
- package/src/seo/core/routeProblems.ts +1 -7
- package/src/seo/core/routeTable.ts +103 -0
- package/src/seo/vite/index.ts +2 -0
- package/src/seo/vite/prerender.ts +26 -1
- package/src/seo/vite/routeTable.ts +79 -0
- package/src/server/answerAddresses.ts +92 -0
- package/src/server/cardMeta.ts +88 -0
- package/src/server/config.ts +125 -0
- package/src/server/envNumber.ts +65 -0
- package/src/server/healthRoute.ts +42 -0
- package/src/server/listenSite.ts +90 -0
- package/src/server/mcpServer.ts +14 -0
- package/src/server/repositoryVersion.ts +33 -0
- package/src/server/routeOptions.ts +26 -0
- package/src/server/routeTable.ts +35 -0
- package/src/server/siteView.ts +42 -0
- package/src/server/spaPages.ts +179 -0
- package/src/server/spaSite.ts +216 -0
- package/src/server/spaSiteOptions.ts +133 -0
- package/src/server/staticSite.ts +65 -35
- package/src/server/toolSiteServer.ts +127 -0
- package/src/server/trustProxy.ts +32 -0
- package/src/server/types.ts +25 -0
- package/src/server.ts +40 -0
- package/src/share/core/config.ts +45 -2
- package/src/share/core/index.ts +4 -0
- package/src/share/core/keySetParam.ts +92 -0
- package/src/share/core/vocabulary.ts +8 -0
- package/src/shell/ui/ShareControl.tsx +150 -0
- package/src/shell/ui/ShellFrame.tsx +180 -0
- package/src/shell/ui/SiteShell.tsx +297 -0
- package/src/shell/ui/index.ts +9 -0
- package/src/state/core/index.ts +1 -0
- package/src/state/core/legacyEntries.ts +76 -0
- package/src/state/core/persistBucket.ts +50 -10
- package/src/state/core/persistSignalBucket.ts +52 -0
- package/src/state/core/storageEntry.ts +32 -0
- package/src/structure/ui/StructureExportButton.tsx +57 -0
- package/src/structure/ui/index.ts +4 -0
- package/src/structure/ui/useStructureDraft.ts +47 -0
- package/src/ui.ts +2 -0
- package/src/worker/core/index.ts +1 -0
- package/src/worker/core/messages.ts +4 -3
- package/src/worker/core/oclResourcesUrl.ts +21 -0
- package/src/worker/core/serveWorkerRequests.ts +30 -2
- package/src/worker/core/workerChannel.ts +74 -74
- package/src/worker/core/workerChannelParts.ts +33 -13
- package/src/worker/core/workerChannelTypes.ts +47 -2
- package/src/worker/core/workerSlot.ts +181 -0
- package/styles/card.css +145 -0
- package/styles/chrome.css +248 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an answer of a site's tool server points: the static site people
|
|
3
|
+
* share, which an answer's `link` names, and this server, whose copy of the
|
|
4
|
+
* same pages a chat frames.
|
|
5
|
+
*
|
|
6
|
+
* The two are written however the deployment writes them — an origin, an
|
|
7
|
+
* origin and a mount, with or without a trailing slash — and a page is named
|
|
8
|
+
* from the site's own root, `/titration` and `titration` alike: every helper
|
|
9
|
+
* joins the two with exactly one slash, so `https://www.cheminfo.org/surge`
|
|
10
|
+
* and `https://www.cheminfo.org/surge/` both put `/generator` at
|
|
11
|
+
* `https://www.cheminfo.org/surge/generator`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { trimTrailingSlash } from '../router/core/address.ts';
|
|
15
|
+
import { joinBasePath } from '../router/core/basePath.ts';
|
|
16
|
+
|
|
17
|
+
/** Where the pages of an answer are served, as a deployment writes them. */
|
|
18
|
+
export interface AnswerAddressesInput {
|
|
19
|
+
/** The static site an answer links to, the address people share. */
|
|
20
|
+
siteUrl: string | URL;
|
|
21
|
+
/** This server, as a browser reaches it: the view frames its pages. */
|
|
22
|
+
publicUrl: string | URL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Where the pages of an answer are served, checked and written alike. */
|
|
26
|
+
export interface AnswerAddresses {
|
|
27
|
+
/** The static site, ending in `/`: `https://equilibrium.cheminfo.org/`. */
|
|
28
|
+
readonly siteUrl: string;
|
|
29
|
+
/** This server, ending in `/`: `https://equilibrium.c6h6.org/`. */
|
|
30
|
+
readonly publicUrl: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check the two addresses of a tool server's answers, and write them alike:
|
|
35
|
+
* absolute, `http` or `https`, without a query or a fragment, ending in `/`.
|
|
36
|
+
* @param input - The site and this server, as the deployment gives them.
|
|
37
|
+
* @returns The two addresses.
|
|
38
|
+
* @throws {Error} When either is not an http or https address.
|
|
39
|
+
*/
|
|
40
|
+
export function answerAddresses(input: AnswerAddressesInput): AnswerAddresses {
|
|
41
|
+
return {
|
|
42
|
+
siteUrl: baseAddress(input.siteUrl, 'siteUrl'),
|
|
43
|
+
publicUrl: baseAddress(input.publicUrl, 'publicUrl'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The origins a view may frame: this server's, which a chat frames, and the
|
|
49
|
+
* shared site's, which a link opens. A server that is the site gives one.
|
|
50
|
+
* @param addresses - The two addresses.
|
|
51
|
+
* @returns Each origin once, this server's first.
|
|
52
|
+
*/
|
|
53
|
+
export function viewOrigins(addresses: AnswerAddressesInput): string[] {
|
|
54
|
+
return [
|
|
55
|
+
...new Set([
|
|
56
|
+
new URL(addresses.publicUrl).origin,
|
|
57
|
+
new URL(addresses.siteUrl).origin,
|
|
58
|
+
]),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A page under a base address, joined with exactly one slash.
|
|
64
|
+
* @param base - An origin, or an origin and a mount, with or without its
|
|
65
|
+
* trailing slash.
|
|
66
|
+
* @param page - A page from the site's own root, with or without its leading
|
|
67
|
+
* slash, and with its query: `/titration?ind=phenolphthalein`.
|
|
68
|
+
* @returns The page's absolute address.
|
|
69
|
+
*/
|
|
70
|
+
export function joinAddress(base: string | URL, page: string): string {
|
|
71
|
+
return `${trimTrailingSlash(String(base))}${joinBasePath('', page)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One address of a deployment, checked.
|
|
76
|
+
* @param value - The address.
|
|
77
|
+
* @param name - What it is, for the error.
|
|
78
|
+
* @returns The address without its query or fragment, ending in `/`.
|
|
79
|
+
*/
|
|
80
|
+
function baseAddress(value: string | URL, name: string): string {
|
|
81
|
+
const text = String(value).trim();
|
|
82
|
+
if (!URL.canParse(text)) {
|
|
83
|
+
throw new Error(`${name} is not an address: ${text}`);
|
|
84
|
+
}
|
|
85
|
+
const url = new URL(text);
|
|
86
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
87
|
+
throw new Error(`${name} is not an http or https address: ${text}`);
|
|
88
|
+
}
|
|
89
|
+
url.search = '';
|
|
90
|
+
url.hash = '';
|
|
91
|
+
return url.href.endsWith('/') ? url.href : `${url.href}/`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool server's answer carries for a chat to show it: the link to the
|
|
3
|
+
* site's page people share, and the `meta` naming that page framed on this
|
|
4
|
+
* server — its card inline, the page itself over the full screen.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CardPageOptions } from '../card/core/cardPageUrl.ts';
|
|
8
|
+
import { cardPageUrl, framedPageUrl } from '../card/core/cardPageUrl.ts';
|
|
9
|
+
import { formatQueryString } from '../router/core/query.ts';
|
|
10
|
+
|
|
11
|
+
import type { AnswerAddressesInput } from './answerAddresses.ts';
|
|
12
|
+
import { joinAddress } from './answerAddresses.ts';
|
|
13
|
+
import { mcpServer } from './mcpServer.ts';
|
|
14
|
+
|
|
15
|
+
/** How {@link cardMeta} writes the card and the page. */
|
|
16
|
+
export interface CardMetaOptions extends CardPageOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The longest card address an answer carries. Browsers take far more, but a
|
|
19
|
+
* proxy reading the request line stops at 8 to 16 KB; past this length the
|
|
20
|
+
* answer carries no card, and the chat draws the answer itself.
|
|
21
|
+
* @default 8000
|
|
22
|
+
*/
|
|
23
|
+
maxLength?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The page an answer is shown on. */
|
|
27
|
+
export interface PageAnswer {
|
|
28
|
+
/** The page on the static site people share: what `result.link` says. */
|
|
29
|
+
link: string;
|
|
30
|
+
/**
|
|
31
|
+
* The answer's `meta`: the card on this server, framed inline, and the same
|
|
32
|
+
* page whole for when the reader asks for more. Undefined when the card's
|
|
33
|
+
* address would be longer than `maxLength`.
|
|
34
|
+
*/
|
|
35
|
+
meta: Record<string, unknown> | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What an answer carries for the view to show a page of this server: inline
|
|
40
|
+
* its card, drawing only the answer; over the full screen the page itself.
|
|
41
|
+
* Both are without the site's chrome, and without the parts `hide` names.
|
|
42
|
+
* @param addresses - Where the site and this server are; only this server's
|
|
43
|
+
* is read.
|
|
44
|
+
* @param page - The page from the site's own root, with its own query and
|
|
45
|
+
* without `embed`, `hide` or `card`: `/titration?ind=phenolphthalein`.
|
|
46
|
+
* @param options - The parts left out, what the card draws, and the longest
|
|
47
|
+
* card address carried.
|
|
48
|
+
* @returns The answer's `meta`, or undefined when the card's address is too
|
|
49
|
+
* long to carry.
|
|
50
|
+
*/
|
|
51
|
+
export function cardMeta(
|
|
52
|
+
addresses: AnswerAddressesInput,
|
|
53
|
+
page: string,
|
|
54
|
+
options: CardMetaOptions = {},
|
|
55
|
+
): Record<string, unknown> | undefined {
|
|
56
|
+
const { maxLength = 8000, ...written } = options;
|
|
57
|
+
const framed = joinAddress(addresses.publicUrl, page);
|
|
58
|
+
const card = cardPageUrl(framed, written);
|
|
59
|
+
if (card.length > maxLength) return undefined;
|
|
60
|
+
return mcpServer().embedMeta(card, { page: framedPageUrl(framed, written) });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A page with its query: the link people share, on the static site, and the
|
|
65
|
+
* `meta` the view frames this server's copy of it with. The query is written
|
|
66
|
+
* by `formatQueryString`, so a `+` is escaped, a comma stays a comma, and a
|
|
67
|
+
* parameter left `undefined` or empty is not written: the page reads its own
|
|
68
|
+
* default.
|
|
69
|
+
* @param addresses - Where the site and this server are.
|
|
70
|
+
* @param path - The page, from the site's own root: `/titration`.
|
|
71
|
+
* @param params - Its query, in the order it is written.
|
|
72
|
+
* @param options - The parts left out, what the card draws, and the longest
|
|
73
|
+
* card address carried.
|
|
74
|
+
* @returns The link and the meta.
|
|
75
|
+
*/
|
|
76
|
+
export function pageAnswer(
|
|
77
|
+
addresses: AnswerAddressesInput,
|
|
78
|
+
path: string,
|
|
79
|
+
params: Readonly<Record<string, string | undefined>> = {},
|
|
80
|
+
options: CardMetaOptions = {},
|
|
81
|
+
): PageAnswer {
|
|
82
|
+
const query = formatQueryString(params);
|
|
83
|
+
const page = query === '' ? path : `${path}?${query}`;
|
|
84
|
+
return {
|
|
85
|
+
link: joinAddress(addresses.siteUrl, page),
|
|
86
|
+
meta: cardMeta(addresses, page, options),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { siteById } from '../ecosystem/core/lookup.ts';
|
|
2
|
+
import type { SiteId } from '../ecosystem/core/sites.ts';
|
|
3
|
+
import { siteUrl } from '../ecosystem/core/sites.ts';
|
|
4
|
+
import { normalizeBasePath } from '../router/core/basePath.ts';
|
|
5
|
+
|
|
6
|
+
import { readEnvNumber } from './envNumber.ts';
|
|
7
|
+
import { parseTrustProxy } from './trustProxy.ts';
|
|
8
|
+
|
|
9
|
+
/** An environment, as `process.env` holds it. */
|
|
10
|
+
export type ServerEnvironment = Readonly<Record<string, string | undefined>>;
|
|
11
|
+
|
|
12
|
+
/** What a site's server knows before it reads its environment. */
|
|
13
|
+
export interface ServerConfigOptions {
|
|
14
|
+
/** The site, as `ECOSYSTEM_SITES` names it: its address is the default `SITE_URL`. */
|
|
15
|
+
siteId: SiteId;
|
|
16
|
+
/** The port derived from the site's creation date, the one compose publishes. */
|
|
17
|
+
port: number;
|
|
18
|
+
/**
|
|
19
|
+
* The build served when `SERVER_ROOT` names none, usually
|
|
20
|
+
* `join(import.meta.dirname, '../dist')`.
|
|
21
|
+
* @default 'dist' - resolved from the directory the process runs in
|
|
22
|
+
*/
|
|
23
|
+
root?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What a site's server is started with, every default filled in. */
|
|
27
|
+
export interface ServerConfig {
|
|
28
|
+
/** Where it listens: `PORT`, or the site's own port. */
|
|
29
|
+
port: number;
|
|
30
|
+
/** The interface it listens on: `HOST`, or every interface, as a container needs. */
|
|
31
|
+
host: string;
|
|
32
|
+
/** `TRACKING_SCRIPT`, verbatim; `undefined` loads nothing. */
|
|
33
|
+
trackingScript: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* `SITE_URL`: the static site people share, which every answer links to,
|
|
36
|
+
* ending in `/`.
|
|
37
|
+
*/
|
|
38
|
+
siteUrl: string;
|
|
39
|
+
/**
|
|
40
|
+
* `PUBLIC_URL`: this server's own address, whose pages a chat frames, ending
|
|
41
|
+
* in `/`.
|
|
42
|
+
*/
|
|
43
|
+
publicUrl: string;
|
|
44
|
+
/** `TRUST_PROXY`: the reverse proxies whose `X-Forwarded-For` is believed. */
|
|
45
|
+
trustProxy: boolean | string;
|
|
46
|
+
/** `SERVER_ROOT`: the build it serves. */
|
|
47
|
+
root: string;
|
|
48
|
+
/**
|
|
49
|
+
* `BASE_PATH`: where the deployment is mounted behind a proxy that takes the
|
|
50
|
+
* path off, `/osiris`, or `''` on a host of its own.
|
|
51
|
+
*/
|
|
52
|
+
basePath: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read a site's server configuration from its environment, once, at startup.
|
|
57
|
+
*
|
|
58
|
+
* A malformed value refuses to start rather than handing out links that lead
|
|
59
|
+
* nowhere. Both addresses keep their path — a site may be mounted under one,
|
|
60
|
+
* and its pages are written relative to it — and lose their query and their
|
|
61
|
+
* fragment; each ends in `/`.
|
|
62
|
+
*
|
|
63
|
+
* const config = readServerConfig(process.env, { siteId: 'osiris', port: 10_620 });
|
|
64
|
+
* @param env - The environment, e.g. `process.env`.
|
|
65
|
+
* @param options - The site and its port.
|
|
66
|
+
* @returns The configuration.
|
|
67
|
+
* @throws {Error} When `PORT`, `SITE_URL` or `PUBLIC_URL` cannot be read, or
|
|
68
|
+
* `TRUST_PROXY` is a hop count.
|
|
69
|
+
*/
|
|
70
|
+
export function readServerConfig(
|
|
71
|
+
env: ServerEnvironment,
|
|
72
|
+
options: ServerConfigOptions,
|
|
73
|
+
): ServerConfig {
|
|
74
|
+
const port = readEnvNumber(env, 'PORT', options.port, {
|
|
75
|
+
min: 1,
|
|
76
|
+
max: 65_535,
|
|
77
|
+
integer: true,
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
port,
|
|
81
|
+
host: env.HOST?.trim() || '0.0.0.0',
|
|
82
|
+
trackingScript: env.TRACKING_SCRIPT?.trim() || undefined,
|
|
83
|
+
siteUrl: readHttpAddress(
|
|
84
|
+
env,
|
|
85
|
+
'SITE_URL',
|
|
86
|
+
siteUrl(siteById(options.siteId)),
|
|
87
|
+
),
|
|
88
|
+
publicUrl: readHttpAddress(env, 'PUBLIC_URL', `http://localhost:${port}/`),
|
|
89
|
+
trustProxy: parseTrustProxy(env.TRUST_PROXY),
|
|
90
|
+
root: env.SERVER_ROOT?.trim() || options.root || 'dist',
|
|
91
|
+
basePath: normalizeBasePath(env.BASE_PATH ?? ''),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read an http or https address from the environment, the way a link is built
|
|
97
|
+
* on it: its path kept and ended with `/`, its query and its fragment dropped.
|
|
98
|
+
* @param env - The environment, e.g. `process.env`.
|
|
99
|
+
* @param name - The variable, e.g. `SITE_URL`.
|
|
100
|
+
* @param fallback - The address when the variable is unset or blank.
|
|
101
|
+
* @returns The address, ending in `/`.
|
|
102
|
+
* @throws {Error} When the value, or the fallback, is not an http or https
|
|
103
|
+
* address.
|
|
104
|
+
*/
|
|
105
|
+
export function readHttpAddress(
|
|
106
|
+
env: ServerEnvironment,
|
|
107
|
+
name: string,
|
|
108
|
+
fallback: string,
|
|
109
|
+
): string {
|
|
110
|
+
const text = env[name]?.trim() || fallback;
|
|
111
|
+
const message = `${name} must be an http(s) address, not "${text}".`;
|
|
112
|
+
let url: URL;
|
|
113
|
+
try {
|
|
114
|
+
url = new URL(text);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(message, { cause: error });
|
|
117
|
+
}
|
|
118
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
119
|
+
throw new Error(message);
|
|
120
|
+
}
|
|
121
|
+
url.search = '';
|
|
122
|
+
url.hash = '';
|
|
123
|
+
if (!url.pathname.endsWith('/')) url.pathname = `${url.pathname}/`;
|
|
124
|
+
return url.href;
|
|
125
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** The bounds a number read from the environment must keep. */
|
|
2
|
+
export interface ReadEnvNumberOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The smallest value accepted.
|
|
5
|
+
* @default undefined - no lower bound
|
|
6
|
+
*/
|
|
7
|
+
min?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The largest value accepted.
|
|
10
|
+
* @default undefined - no upper bound
|
|
11
|
+
*/
|
|
12
|
+
max?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the value must be a whole number.
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
integer?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read a number from the environment.
|
|
22
|
+
*
|
|
23
|
+
* The compose files forward every variable as `${NAME:-}`, so a deployment that
|
|
24
|
+
* sets nothing hands the process an empty string rather than no variable — and
|
|
25
|
+
* `Number('')` is 0, which turned an indexer's interval into a busy loop. Unset,
|
|
26
|
+
* empty and blank all mean the fallback here. A value that says something but is
|
|
27
|
+
* not an accepted number refuses to start rather than running on a guess.
|
|
28
|
+
* @param env - The environment, e.g. `process.env`.
|
|
29
|
+
* @param name - The variable, e.g. `INDEX_INTERVAL`.
|
|
30
|
+
* @param fallback - The value when the variable says nothing.
|
|
31
|
+
* @param options - The bounds the value must keep.
|
|
32
|
+
* @returns The number written, or the fallback.
|
|
33
|
+
* @throws {Error} When the value is not a finite number, or breaks a bound.
|
|
34
|
+
*/
|
|
35
|
+
export function readEnvNumber(
|
|
36
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
37
|
+
name: string,
|
|
38
|
+
fallback: number,
|
|
39
|
+
options: ReadEnvNumberOptions = {},
|
|
40
|
+
): number {
|
|
41
|
+
const text = env[name]?.trim();
|
|
42
|
+
if (!text) return fallback;
|
|
43
|
+
const value = Number(text);
|
|
44
|
+
const { min, max, integer = false } = options;
|
|
45
|
+
if (
|
|
46
|
+
!Number.isFinite(value) ||
|
|
47
|
+
(integer && !Number.isInteger(value)) ||
|
|
48
|
+
(min !== undefined && value < min) ||
|
|
49
|
+
(max !== undefined && value > max)
|
|
50
|
+
) {
|
|
51
|
+
throw new Error(`${name} must be ${expected(options)}, not "${text}".`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function expected(options: ReadEnvNumberOptions): string {
|
|
57
|
+
const { min, max, integer = false } = options;
|
|
58
|
+
const kind = integer ? 'an integer' : 'a number';
|
|
59
|
+
if (min !== undefined && max !== undefined) {
|
|
60
|
+
return `${kind} from ${min} to ${max}`;
|
|
61
|
+
}
|
|
62
|
+
if (min !== undefined) return `${kind} of at least ${min}`;
|
|
63
|
+
if (max !== undefined) return `${kind} of at most ${max}`;
|
|
64
|
+
return kind;
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
import { ALLOW_ORIGIN, HIDDEN } from './routeOptions.ts';
|
|
4
|
+
|
|
5
|
+
/** Options of {@link healthRoute}. */
|
|
6
|
+
export interface HealthRouteOptions {
|
|
7
|
+
/**
|
|
8
|
+
* More fields for the answer, read locally and cheaply: a probe every 30 s
|
|
9
|
+
* must never wait on another service, or a dependency that is down rolls back
|
|
10
|
+
* a good release. `status` is always `ok`, whatever this returns.
|
|
11
|
+
* @default undefined - the answer is `{ status: 'ok' }`
|
|
12
|
+
*/
|
|
13
|
+
extra?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Answer `GET /health`, what the container's health check and the server's
|
|
18
|
+
* deploy script probe before they keep a new container.
|
|
19
|
+
*
|
|
20
|
+
* It is at the root, never versioned, because it probes the process rather than
|
|
21
|
+
* the API contract. It logs nothing, or a probe every 30 s fills the log; it is
|
|
22
|
+
* kept out of the API's documentation; and it is open to any origin, like
|
|
23
|
+
* everything else a site serves.
|
|
24
|
+
*
|
|
25
|
+
* await healthRoute(app);
|
|
26
|
+
* @param fastify - The server.
|
|
27
|
+
* @param options - Extra fields for the answer.
|
|
28
|
+
*/
|
|
29
|
+
export async function healthRoute(
|
|
30
|
+
fastify: FastifyInstance,
|
|
31
|
+
options: HealthRouteOptions = {},
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { extra } = options;
|
|
34
|
+
fastify.get(
|
|
35
|
+
'/health',
|
|
36
|
+
{ logLevel: 'silent', schema: HIDDEN },
|
|
37
|
+
async (_request, reply) => {
|
|
38
|
+
const fields = extra === undefined ? {} : await extra();
|
|
39
|
+
return reply.header(ALLOW_ORIGIN, '*').send({ ...fields, status: 'ok' });
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
/** Options of {@link listenSite}. */
|
|
4
|
+
export interface ListenSiteOptions {
|
|
5
|
+
/** The port to listen on. */
|
|
6
|
+
port: number;
|
|
7
|
+
/**
|
|
8
|
+
* The interface to listen on.
|
|
9
|
+
* @default '0.0.0.0' - every interface, as a container needs
|
|
10
|
+
*/
|
|
11
|
+
host?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Run once the server has closed on a signal: stop what the server does not
|
|
14
|
+
* own itself, such as a scheduler or a write queue started beside it.
|
|
15
|
+
* @default undefined - nothing more is stopped
|
|
16
|
+
*/
|
|
17
|
+
onClose?: () => void | Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* The signals that close the server.
|
|
20
|
+
* @default ['SIGTERM', 'SIGINT'] - what `docker stop` and a terminal send
|
|
21
|
+
*/
|
|
22
|
+
signals?: readonly NodeJS.Signals[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Listen, and close cleanly when the container is stopped.
|
|
27
|
+
*
|
|
28
|
+
* On the first signal the server closes — its `onClose` hooks stop the worker
|
|
29
|
+
* pools it owns — then `onClose` runs. A close that fails is logged and leaves
|
|
30
|
+
* `process.exitCode` at 1 rather than surfacing as an unhandled rejection, and
|
|
31
|
+
* the process ends on its own once nothing is left running. A server that
|
|
32
|
+
* cannot listen is logged, closed and reported the same way.
|
|
33
|
+
*
|
|
34
|
+
* await listenSite(app, { port: config.port, host: config.host });
|
|
35
|
+
* @param app - The server, built and not yet listening.
|
|
36
|
+
* @param options - Where to listen, and what else to stop.
|
|
37
|
+
* @throws {Error} When the server cannot listen, after it has been closed.
|
|
38
|
+
*/
|
|
39
|
+
export async function listenSite(
|
|
40
|
+
app: FastifyInstance,
|
|
41
|
+
options: ListenSiteOptions,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const {
|
|
44
|
+
port,
|
|
45
|
+
host = '0.0.0.0',
|
|
46
|
+
onClose,
|
|
47
|
+
signals = ['SIGTERM', 'SIGINT'],
|
|
48
|
+
} = options;
|
|
49
|
+
|
|
50
|
+
const stop = () => {
|
|
51
|
+
for (const signal of signals) process.off(signal, stop);
|
|
52
|
+
void shutDown(app, onClose);
|
|
53
|
+
};
|
|
54
|
+
for (const signal of signals) process.once(signal, stop);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await app.listen({ port, host });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
for (const signal of signals) process.off(signal, stop);
|
|
60
|
+
app.log.error(error, `the server could not listen on ${host}:${port}`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
await shutDown(app, onClose);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Close the server, then run what else has to stop, reporting a failure of
|
|
69
|
+
* either through the log and the exit code.
|
|
70
|
+
* @param app - The server.
|
|
71
|
+
* @param onClose - What else to stop.
|
|
72
|
+
*/
|
|
73
|
+
async function shutDown(
|
|
74
|
+
app: FastifyInstance,
|
|
75
|
+
onClose: (() => void | Promise<void>) | undefined,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
await app.close();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
app.log.error(error, 'the server did not close cleanly');
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
if (onClose === undefined) return;
|
|
84
|
+
try {
|
|
85
|
+
await onClose();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
app.log.error(error, 'what runs beside the server did not stop cleanly');
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
import type * as McpServer from 'cheminfo-mcp-server';
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `cheminfo-mcp-server`, an optional peer, loaded when a helper first needs
|
|
9
|
+
* it, so `react-cheminfo/server` imports on a site that serves no tools.
|
|
10
|
+
* @returns The module.
|
|
11
|
+
*/
|
|
12
|
+
export function mcpServer(): typeof McpServer {
|
|
13
|
+
return require('cheminfo-mcp-server') as typeof McpServer;
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { UNRELEASED_VERSION } from '../build/core/buildInfo.ts';
|
|
5
|
+
import { findRepositoryRoot } from '../build/vite/buildInfo.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The release a server's repository is at, as release-please writes it.
|
|
9
|
+
*
|
|
10
|
+
* Read from the `package.json` at the repository root, found by the same
|
|
11
|
+
* climbing rule `cheminfoBuildInfo` follows: a `backend` or `server` directory
|
|
12
|
+
* keeps the `0.0.0` it was scaffolded with, or none at all, while the root is
|
|
13
|
+
* the manifest a release bumps. A manifest that cannot be read is not a reason
|
|
14
|
+
* to refuse to start, so it reads as unreleased instead.
|
|
15
|
+
*
|
|
16
|
+
* const version = readRepositoryVersion(import.meta.dirname);
|
|
17
|
+
* @param fromDirectory - Where to start climbing, usually the server's own
|
|
18
|
+
* directory.
|
|
19
|
+
* @returns The version, or `'0.0.0'` when the root manifest carries none.
|
|
20
|
+
*/
|
|
21
|
+
export function readRepositoryVersion(fromDirectory: string): string {
|
|
22
|
+
const root = findRepositoryRoot(fromDirectory);
|
|
23
|
+
let manifest: unknown;
|
|
24
|
+
try {
|
|
25
|
+
manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return UNRELEASED_VERSION;
|
|
28
|
+
}
|
|
29
|
+
const version = (manifest as { version?: unknown } | null)?.version;
|
|
30
|
+
return typeof version === 'string' && version !== ''
|
|
31
|
+
? version
|
|
32
|
+
: UNRELEASED_VERSION;
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every route the serving helpers register shares: how it is cached, who
|
|
3
|
+
* may read it, and that it stays out of the API's documentation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FastifySchema } from 'fastify';
|
|
7
|
+
|
|
8
|
+
/** A page is checked on every visit: it names the bundles of its own build. */
|
|
9
|
+
export const PAGE_CACHE = 'no-cache';
|
|
10
|
+
|
|
11
|
+
/** A bundle is named by its content, so it never changes under its name. */
|
|
12
|
+
export const BUNDLE_CACHE = 'public, max-age=31536000, immutable';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The header that opens an answer to every origin. Every file of a build is
|
|
16
|
+
* public, and a page framed by a sandboxed view runs under an opaque origin,
|
|
17
|
+
* where everything it fetches from its own site is cross-origin: the static
|
|
18
|
+
* hosts open every response, and so do these servers.
|
|
19
|
+
*/
|
|
20
|
+
export const ALLOW_ORIGIN = 'access-control-allow-origin';
|
|
21
|
+
|
|
22
|
+
/** Kept out of the API's documentation (`@fastify/swagger` reads `hide`). */
|
|
23
|
+
export const HIDDEN: FastifySchema & { hide: boolean } = { hide: true };
|
|
24
|
+
|
|
25
|
+
/** The addresses an API and its documentation answer, never a page. */
|
|
26
|
+
export const DEFAULT_RESERVED: readonly string[] = ['/v1/', '/docs'];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ROUTE_TABLE_FILE, parseRouteTable } from '../seo/core/routeTable.ts';
|
|
5
|
+
import type { RouteMeta } from '../seo/core/routes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every address the frontend routes itself, from the `routes.json` its build
|
|
9
|
+
* wrote beside the page (`cheminfoRouteTable` in `react-cheminfo/vite`).
|
|
10
|
+
*
|
|
11
|
+
* The pages that exist whatever the build wrote are the fallback, so a server
|
|
12
|
+
* started before the frontend was built, or against a build of an older shape,
|
|
13
|
+
* still titles them.
|
|
14
|
+
* @param root - The directory holding the built frontend.
|
|
15
|
+
* @param fallback - The routes when the build wrote no table this can read.
|
|
16
|
+
* @returns The routes, the home page first.
|
|
17
|
+
*/
|
|
18
|
+
export function readRouteTable(
|
|
19
|
+
root: string,
|
|
20
|
+
fallback: readonly RouteMeta[],
|
|
21
|
+
): RouteMeta[] {
|
|
22
|
+
let text: string;
|
|
23
|
+
try {
|
|
24
|
+
text = readFileSync(join(root, ROUTE_TABLE_FILE), 'utf8');
|
|
25
|
+
} catch {
|
|
26
|
+
return [...fallback];
|
|
27
|
+
}
|
|
28
|
+
let parsed: unknown;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(text);
|
|
31
|
+
} catch {
|
|
32
|
+
return [...fallback];
|
|
33
|
+
}
|
|
34
|
+
return parseRouteTable(parsed) ?? [...fallback];
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AppResource } from 'cheminfo-mcp-server';
|
|
2
|
+
|
|
3
|
+
import type { AnswerAddressesInput } from './answerAddresses.ts';
|
|
4
|
+
import { viewOrigins } from './answerAddresses.ts';
|
|
5
|
+
import { mcpServer } from './mcpServer.ts';
|
|
6
|
+
|
|
7
|
+
/** What {@link siteView} names, and where its pages come from. */
|
|
8
|
+
export interface SiteViewOptions {
|
|
9
|
+
/** Its address, which the tools name in `ui.resourceUri`: `ui://lcao/page`. */
|
|
10
|
+
uri: string;
|
|
11
|
+
/** For people: the site's host, `lcao.cheminfo.org`. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** For people: what the card shows inline, and the page over the screen. */
|
|
14
|
+
description: string;
|
|
15
|
+
/** Where the site and this server are: the view frames both, and no other. */
|
|
16
|
+
addresses: AnswerAddressesInput;
|
|
17
|
+
/**
|
|
18
|
+
* The height of the frame, in pixels, for a page that cannot report its
|
|
19
|
+
* own.
|
|
20
|
+
* @default undefined - the page reports its height, as `CardFrame` does
|
|
21
|
+
*/
|
|
22
|
+
height?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The MCP App showing an answer on the site's own page: inline the answer's
|
|
27
|
+
* card, the page itself over the full screen, framed from this server or from
|
|
28
|
+
* the static site. One per tool server, named by every tool whose answer
|
|
29
|
+
* carries a `meta` from `cardMeta` or `pageAnswer`.
|
|
30
|
+
* @param options - Its address, its name, and the two addresses it frames.
|
|
31
|
+
* @returns The resource, for `createToolRoutes({ resources })`.
|
|
32
|
+
*/
|
|
33
|
+
export function siteView(options: SiteViewOptions): AppResource {
|
|
34
|
+
const { uri, name, description, addresses, height } = options;
|
|
35
|
+
return mcpServer().embedView({
|
|
36
|
+
uri,
|
|
37
|
+
name,
|
|
38
|
+
description,
|
|
39
|
+
origins: viewOrigins(addresses),
|
|
40
|
+
height,
|
|
41
|
+
});
|
|
42
|
+
}
|