@buildcanada/charts 0.1.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/LICENSE.md +8 -0
- package/README.md +113 -0
- package/package.json +137 -0
- package/src/components/BodyPortal/BodyPortal.tsx +40 -0
- package/src/components/Button/Button.scss +110 -0
- package/src/components/Button/Button.tsx +101 -0
- package/src/components/Checkbox.scss +93 -0
- package/src/components/Checkbox.tsx +47 -0
- package/src/components/ExpandableToggle/ExpandableToggle.scss +123 -0
- package/src/components/ExpandableToggle/ExpandableToggle.tsx +60 -0
- package/src/components/GrapherTabIcon.tsx +156 -0
- package/src/components/GrapherTrendArrow.scss +16 -0
- package/src/components/GrapherTrendArrow.tsx +30 -0
- package/src/components/Halo/Halo.tsx +44 -0
- package/src/components/LabeledSwitch/LabeledSwitch.scss +109 -0
- package/src/components/LabeledSwitch/LabeledSwitch.tsx +62 -0
- package/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx +1173 -0
- package/src/components/OverlayHeader.scss +18 -0
- package/src/components/OverlayHeader.tsx +29 -0
- package/src/components/RadioButton.scss +69 -0
- package/src/components/RadioButton.tsx +42 -0
- package/src/components/SimpleMarkdownText.tsx +89 -0
- package/src/components/TextInput.scss +17 -0
- package/src/components/TextInput.tsx +19 -0
- package/src/components/TextWrap/TextWrap.tsx +361 -0
- package/src/components/TextWrap/TextWrapUtils.ts +32 -0
- package/src/components/closeButton/CloseButton.scss +40 -0
- package/src/components/closeButton/CloseButton.tsx +27 -0
- package/src/components/index.ts +70 -0
- package/src/components/loadingIndicator/LoadingIndicator.scss +40 -0
- package/src/components/loadingIndicator/LoadingIndicator.tsx +28 -0
- package/src/components/markdown/remarkPlainLinks.ts +36 -0
- package/src/components/reactUtil.ts +20 -0
- package/src/components/stubs/CodeSnippet.tsx +19 -0
- package/src/components/stubs/DataCitation.tsx +16 -0
- package/src/components/stubs/IndicatorKeyData.tsx +45 -0
- package/src/components/stubs/IndicatorProcessing.tsx +15 -0
- package/src/components/stubs/IndicatorSources.tsx +15 -0
- package/src/components/styles/colors.scss +113 -0
- package/src/components/styles/mixins.scss +630 -0
- package/src/components/styles/typography.scss +579 -0
- package/src/components/styles/util.scss +89 -0
- package/src/components/styles/variables.scss +208 -0
- package/src/config/ChartsConfig.ts +163 -0
- package/src/config/ChartsProvider.tsx +157 -0
- package/src/config/index.ts +20 -0
- package/src/core-table/CoreTable.ts +1355 -0
- package/src/core-table/CoreTableColumns.ts +973 -0
- package/src/core-table/CoreTableUtils.ts +793 -0
- package/src/core-table/ErrorValues.ts +73 -0
- package/src/core-table/OwidTable.ts +1175 -0
- package/src/core-table/OwidTableSynthesizers.ts +272 -0
- package/src/core-table/OwidTableUtil.ts +76 -0
- package/src/core-table/Transforms.ts +484 -0
- package/src/core-table/index.ts +82 -0
- package/src/explorer/ColumnGrammar.ts +217 -0
- package/src/explorer/Explorer.sample.ts +212 -0
- package/src/explorer/Explorer.scss +148 -0
- package/src/explorer/Explorer.tsx +1283 -0
- package/src/explorer/ExplorerConstants.ts +85 -0
- package/src/explorer/ExplorerControls.scss +156 -0
- package/src/explorer/ExplorerControls.tsx +210 -0
- package/src/explorer/ExplorerDecisionMatrix.ts +471 -0
- package/src/explorer/ExplorerGrammar.ts +161 -0
- package/src/explorer/ExplorerProgram.ts +568 -0
- package/src/explorer/ExplorerUtils.ts +59 -0
- package/src/explorer/GrapherGrammar.ts +387 -0
- package/src/explorer/gridLang/GrammarUtils.ts +121 -0
- package/src/explorer/gridLang/GridCell.ts +298 -0
- package/src/explorer/gridLang/GridLangConstants.ts +255 -0
- package/src/explorer/gridLang/GridProgram.ts +311 -0
- package/src/explorer/gridLang/readme.md +17 -0
- package/src/explorer/index.ts +69 -0
- package/src/explorer/readme.md +19 -0
- package/src/explorer/urlMigrations/CO2UrlMigration.ts +46 -0
- package/src/explorer/urlMigrations/CovidUrlMigration.ts +37 -0
- package/src/explorer/urlMigrations/EnergyUrlMigration.ts +41 -0
- package/src/explorer/urlMigrations/ExplorerPageUrlMigrationSpec.ts +12 -0
- package/src/explorer/urlMigrations/ExplorerUrlMigrationUtils.ts +45 -0
- package/src/explorer/urlMigrations/ExplorerUrlMigrations.ts +33 -0
- package/src/explorer/urlMigrations/LegacyCovidUrlMigration.ts +144 -0
- package/src/explorer/urlMigrations/readme.md +39 -0
- package/src/grapher/axis/Axis.ts +973 -0
- package/src/grapher/axis/AxisConfig.ts +179 -0
- package/src/grapher/axis/AxisViews.tsx +597 -0
- package/src/grapher/barCharts/DiscreteBarChart.tsx +728 -0
- package/src/grapher/barCharts/DiscreteBarChartConstants.ts +60 -0
- package/src/grapher/barCharts/DiscreteBarChartHelpers.ts +338 -0
- package/src/grapher/barCharts/DiscreteBarChartState.ts +354 -0
- package/src/grapher/barCharts/DiscreteBarChartThumbnail.tsx +34 -0
- package/src/grapher/captionedChart/CaptionedChart.scss +61 -0
- package/src/grapher/captionedChart/CaptionedChart.tsx +523 -0
- package/src/grapher/captionedChart/Logos.tsx +141 -0
- package/src/grapher/captionedChart/LogosSVG.tsx +16 -0
- package/src/grapher/captionedChart/StaticChartRasterizer.tsx +178 -0
- package/src/grapher/captionedChart/assets/buildcanada-logo-square.svg +15 -0
- package/src/grapher/captionedChart/assets/buildcanada-logo.svg +15 -0
- package/src/grapher/captionedChart/assets/canadaspends.svg +7 -0
- package/src/grapher/captionedChart/readme.md +14 -0
- package/src/grapher/chart/Chart.tsx +62 -0
- package/src/grapher/chart/ChartAreaContent.tsx +172 -0
- package/src/grapher/chart/ChartDimension.ts +121 -0
- package/src/grapher/chart/ChartInterface.ts +83 -0
- package/src/grapher/chart/ChartManager.ts +113 -0
- package/src/grapher/chart/ChartTabs.ts +178 -0
- package/src/grapher/chart/ChartTypeMap.tsx +158 -0
- package/src/grapher/chart/ChartTypeSwitcher.tsx +26 -0
- package/src/grapher/chart/ChartUtils.tsx +364 -0
- package/src/grapher/chart/DimensionSlot.ts +45 -0
- package/src/grapher/chart/StaticChartWrapper.tsx +94 -0
- package/src/grapher/chart/guidedChartUtils.ts +82 -0
- package/src/grapher/color/BinningStrategies.ts +484 -0
- package/src/grapher/color/BinningStrategyEqualSizeBins.ts +132 -0
- package/src/grapher/color/BinningStrategyLogarithmic.ts +121 -0
- package/src/grapher/color/CategoricalColorAssigner.ts +97 -0
- package/src/grapher/color/ColorBrewerSchemes.ts +80 -0
- package/src/grapher/color/ColorConstants.ts +20 -0
- package/src/grapher/color/ColorScale.ts +339 -0
- package/src/grapher/color/ColorScaleBin.ts +147 -0
- package/src/grapher/color/ColorScaleConfig.ts +204 -0
- package/src/grapher/color/ColorScheme.ts +137 -0
- package/src/grapher/color/ColorSchemes.ts +149 -0
- package/src/grapher/color/ColorUtils.ts +86 -0
- package/src/grapher/color/CustomSchemes.ts +1772 -0
- package/src/grapher/color/readme.md +84 -0
- package/src/grapher/comparisonLine/ComparisonLine.tsx +31 -0
- package/src/grapher/comparisonLine/ComparisonLineConstants.ts +11 -0
- package/src/grapher/comparisonLine/ComparisonLineGenerator.ts +60 -0
- package/src/grapher/comparisonLine/ComparisonLineHelpers.ts +10 -0
- package/src/grapher/comparisonLine/CustomComparisonLine.tsx +159 -0
- package/src/grapher/comparisonLine/VerticalComparisonLine.tsx +208 -0
- package/src/grapher/controls/ActionButtons.scss +97 -0
- package/src/grapher/controls/ActionButtons.tsx +453 -0
- package/src/grapher/controls/CommandPalette.scss +50 -0
- package/src/grapher/controls/CommandPalette.tsx +74 -0
- package/src/grapher/controls/ContentSwitchers.scss +93 -0
- package/src/grapher/controls/ContentSwitchers.tsx +238 -0
- package/src/grapher/controls/Controls.scss +158 -0
- package/src/grapher/controls/DataTableFilterDropdown.scss +7 -0
- package/src/grapher/controls/DataTableFilterDropdown.tsx +168 -0
- package/src/grapher/controls/DataTableSearchField.scss +3 -0
- package/src/grapher/controls/DataTableSearchField.tsx +76 -0
- package/src/grapher/controls/Dropdown.scss +252 -0
- package/src/grapher/controls/Dropdown.tsx +235 -0
- package/src/grapher/controls/EntitySelectionToggle.tsx +135 -0
- package/src/grapher/controls/MapRegionDropdown.scss +3 -0
- package/src/grapher/controls/MapRegionDropdown.tsx +104 -0
- package/src/grapher/controls/MapResetButton.tsx +115 -0
- package/src/grapher/controls/MapZoomDropdown.scss +9 -0
- package/src/grapher/controls/MapZoomDropdown.tsx +270 -0
- package/src/grapher/controls/MapZoomToSelectionButton.tsx +87 -0
- package/src/grapher/controls/SearchField.scss +78 -0
- package/src/grapher/controls/SearchField.tsx +63 -0
- package/src/grapher/controls/SettingsMenu.scss +191 -0
- package/src/grapher/controls/SettingsMenu.tsx +399 -0
- package/src/grapher/controls/ShareMenu.scss +58 -0
- package/src/grapher/controls/ShareMenu.tsx +304 -0
- package/src/grapher/controls/SortIcon.tsx +39 -0
- package/src/grapher/controls/VerticalScrollContainer.tsx +263 -0
- package/src/grapher/controls/controlsRow/ControlsRow.tsx +168 -0
- package/src/grapher/controls/dropdown-icons.scss +4 -0
- package/src/grapher/controls/entityPicker/EntityPicker.scss +255 -0
- package/src/grapher/controls/entityPicker/EntityPicker.tsx +816 -0
- package/src/grapher/controls/entityPicker/EntityPickerConstants.ts +23 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.scss +129 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.tsx +463 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelectorConstants.ts +3 -0
- package/src/grapher/controls/globalEntitySelector/readme.md +17 -0
- package/src/grapher/controls/settings/AbsRelToggle.tsx +64 -0
- package/src/grapher/controls/settings/AxisScaleToggle.tsx +53 -0
- package/src/grapher/controls/settings/FacetStrategySelector.tsx +110 -0
- package/src/grapher/controls/settings/FacetYDomainToggle.tsx +51 -0
- package/src/grapher/controls/settings/NoDataAreaToggle.tsx +38 -0
- package/src/grapher/controls/settings/ZoomToggle.tsx +36 -0
- package/src/grapher/core/EntitiesByRegionType.ts +174 -0
- package/src/grapher/core/EntityCodes.ts +19 -0
- package/src/grapher/core/EntityUrlBuilder.ts +200 -0
- package/src/grapher/core/FetchingGrapher.tsx +156 -0
- package/src/grapher/core/Grapher.tsx +760 -0
- package/src/grapher/core/GrapherAnalytics.ts +229 -0
- package/src/grapher/core/GrapherConstants.ts +173 -0
- package/src/grapher/core/GrapherState.tsx +3659 -0
- package/src/grapher/core/GrapherUrl.ts +184 -0
- package/src/grapher/core/GrapherUrlMigrations.ts +29 -0
- package/src/grapher/core/GrapherUseHelpers.tsx +147 -0
- package/src/grapher/core/LegacyToOwidTable.ts +841 -0
- package/src/grapher/core/grapher.entry.ts +5 -0
- package/src/grapher/core/grapher.scss +257 -0
- package/src/grapher/core/loadGrapherTableHelpers.ts +116 -0
- package/src/grapher/core/loadVariable.ts +104 -0
- package/src/grapher/core/relatedQuestion.ts +12 -0
- package/src/grapher/core/typography.scss +206 -0
- package/src/grapher/dataTable/DataTable.sample.ts +206 -0
- package/src/grapher/dataTable/DataTable.scss +249 -0
- package/src/grapher/dataTable/DataTable.tsx +1332 -0
- package/src/grapher/dataTable/DataTableConstants.ts +186 -0
- package/src/grapher/entitySelector/EntitySelector.scss +255 -0
- package/src/grapher/entitySelector/EntitySelector.tsx +1838 -0
- package/src/grapher/facet/FacetChart.tsx +943 -0
- package/src/grapher/facet/FacetChartConstants.ts +24 -0
- package/src/grapher/facet/FacetChartUtils.ts +51 -0
- package/src/grapher/facet/FacetMap.tsx +604 -0
- package/src/grapher/facet/FacetMapConstants.ts +23 -0
- package/src/grapher/facet/readme.md +13 -0
- package/src/grapher/focus/FocusArray.ts +79 -0
- package/src/grapher/footer/Footer.scss +63 -0
- package/src/grapher/footer/Footer.tsx +809 -0
- package/src/grapher/footer/FooterManager.ts +44 -0
- package/src/grapher/fullScreen/FullScreen.scss +11 -0
- package/src/grapher/fullScreen/FullScreen.tsx +61 -0
- package/src/grapher/header/Header.scss +35 -0
- package/src/grapher/header/Header.tsx +372 -0
- package/src/grapher/header/HeaderManager.ts +28 -0
- package/src/grapher/index.ts +157 -0
- package/src/grapher/interaction/InteractionState.ts +60 -0
- package/src/grapher/legend/HorizontalColorLegends.tsx +923 -0
- package/src/grapher/legend/LegendInteractionState.ts +40 -0
- package/src/grapher/legend/VerticalColorLegend.tsx +295 -0
- package/src/grapher/lineCharts/LineChart.tsx +968 -0
- package/src/grapher/lineCharts/LineChartConstants.ts +89 -0
- package/src/grapher/lineCharts/LineChartHelpers.ts +184 -0
- package/src/grapher/lineCharts/LineChartState.ts +394 -0
- package/src/grapher/lineCharts/LineChartThumbnail.tsx +437 -0
- package/src/grapher/lineCharts/Lines.tsx +258 -0
- package/src/grapher/lineLegend/LineLegend.tsx +723 -0
- package/src/grapher/lineLegend/LineLegendConstants.ts +9 -0
- package/src/grapher/lineLegend/LineLegendFilterAlgorithms.ts +143 -0
- package/src/grapher/lineLegend/LineLegendHelpers.ts +253 -0
- package/src/grapher/lineLegend/LineLegendTypes.ts +32 -0
- package/src/grapher/mapCharts/CanadaTopology.ts +17922 -0
- package/src/grapher/mapCharts/ChoroplethGlobe.tsx +949 -0
- package/src/grapher/mapCharts/ChoroplethMap.tsx +662 -0
- package/src/grapher/mapCharts/GeoFeatures.ts +184 -0
- package/src/grapher/mapCharts/GlobeController.ts +496 -0
- package/src/grapher/mapCharts/MapAnnotationPlacements.json +1040 -0
- package/src/grapher/mapCharts/MapAnnotationPlacements.ts +31 -0
- package/src/grapher/mapCharts/MapAnnotations.ts +723 -0
- package/src/grapher/mapCharts/MapChart.sample.ts +59 -0
- package/src/grapher/mapCharts/MapChart.scss +5 -0
- package/src/grapher/mapCharts/MapChart.tsx +720 -0
- package/src/grapher/mapCharts/MapChartConstants.ts +260 -0
- package/src/grapher/mapCharts/MapChartState.ts +416 -0
- package/src/grapher/mapCharts/MapChartThumbnail.tsx +25 -0
- package/src/grapher/mapCharts/MapComponents.tsx +338 -0
- package/src/grapher/mapCharts/MapConfig.ts +156 -0
- package/src/grapher/mapCharts/MapHelpers.ts +181 -0
- package/src/grapher/mapCharts/MapProjections.ts +49 -0
- package/src/grapher/mapCharts/MapSparkline.tsx +257 -0
- package/src/grapher/mapCharts/MapTooltip.scss +49 -0
- package/src/grapher/mapCharts/MapTooltip.tsx +409 -0
- package/src/grapher/mapCharts/MapTopology.ts +1766 -0
- package/src/grapher/mapCharts/d3-bboxCollide.js +204 -0
- package/src/grapher/mapCharts/d3-geo-projection.ts +198 -0
- package/src/grapher/modal/DownloadIcons.tsx +39 -0
- package/src/grapher/modal/DownloadModal.scss +300 -0
- package/src/grapher/modal/DownloadModal.tsx +1226 -0
- package/src/grapher/modal/EmbedModal.scss +40 -0
- package/src/grapher/modal/EmbedModal.tsx +160 -0
- package/src/grapher/modal/EntitySelectorModal.tsx +59 -0
- package/src/grapher/modal/Modal.scss +31 -0
- package/src/grapher/modal/Modal.tsx +90 -0
- package/src/grapher/modal/ModalHeader.scss +12 -0
- package/src/grapher/modal/ModalHeader.tsx +16 -0
- package/src/grapher/modal/SourcesDescriptions.scss +87 -0
- package/src/grapher/modal/SourcesDescriptions.tsx +89 -0
- package/src/grapher/modal/SourcesKeyDataTable.scss +49 -0
- package/src/grapher/modal/SourcesKeyDataTable.tsx +87 -0
- package/src/grapher/modal/SourcesModal.scss +301 -0
- package/src/grapher/modal/SourcesModal.tsx +568 -0
- package/src/grapher/noDataModal/NoDataModal.tsx +125 -0
- package/src/grapher/scatterCharts/ConnectedScatterLegend.tsx +143 -0
- package/src/grapher/scatterCharts/MultiColorPolyline.tsx +129 -0
- package/src/grapher/scatterCharts/NoDataSection.scss +14 -0
- package/src/grapher/scatterCharts/NoDataSection.tsx +56 -0
- package/src/grapher/scatterCharts/ScatterPlotChart.tsx +792 -0
- package/src/grapher/scatterCharts/ScatterPlotChartConstants.ts +157 -0
- package/src/grapher/scatterCharts/ScatterPlotChartState.ts +678 -0
- package/src/grapher/scatterCharts/ScatterPlotChartThumbnail.tsx +155 -0
- package/src/grapher/scatterCharts/ScatterPlotTooltip.tsx +560 -0
- package/src/grapher/scatterCharts/ScatterPoints.tsx +153 -0
- package/src/grapher/scatterCharts/ScatterPointsWithLabels.tsx +708 -0
- package/src/grapher/scatterCharts/ScatterSizeLegend.tsx +327 -0
- package/src/grapher/scatterCharts/ScatterUtils.ts +265 -0
- package/src/grapher/scatterCharts/Triangle.tsx +41 -0
- package/src/grapher/schema/README.md +33 -0
- package/src/grapher/schema/defaultGrapherConfig.ts +100 -0
- package/src/grapher/schema/grapher-schema.009.yaml +781 -0
- package/src/grapher/schema/migrations/helpers.ts +58 -0
- package/src/grapher/schema/migrations/migrate.ts +75 -0
- package/src/grapher/schema/migrations/migrations.ts +158 -0
- package/src/grapher/selection/MapSelectionArray.ts +99 -0
- package/src/grapher/selection/SelectionArray.ts +71 -0
- package/src/grapher/selection/readme.md +16 -0
- package/src/grapher/sidePanel/SidePanel.scss +10 -0
- package/src/grapher/sidePanel/SidePanel.tsx +23 -0
- package/src/grapher/slideInDrawer/SlideInDrawer.scss +57 -0
- package/src/grapher/slideInDrawer/SlideInDrawer.tsx +125 -0
- package/src/grapher/slideshowController/SlideShowController.tsx +43 -0
- package/src/grapher/slideshowController/readme.md +7 -0
- package/src/grapher/slopeCharts/MarkX.tsx +45 -0
- package/src/grapher/slopeCharts/Slope.tsx +102 -0
- package/src/grapher/slopeCharts/SlopeChart.tsx +1152 -0
- package/src/grapher/slopeCharts/SlopeChartConstants.ts +33 -0
- package/src/grapher/slopeCharts/SlopeChartHelpers.ts +73 -0
- package/src/grapher/slopeCharts/SlopeChartState.ts +392 -0
- package/src/grapher/slopeCharts/SlopeChartThumbnail.tsx +368 -0
- package/src/grapher/stackedCharts/AbstractStackedChartState.ts +370 -0
- package/src/grapher/stackedCharts/MarimekkoBars.tsx +190 -0
- package/src/grapher/stackedCharts/MarimekkoBarsForOneEntity.tsx +168 -0
- package/src/grapher/stackedCharts/MarimekkoChart.tsx +1144 -0
- package/src/grapher/stackedCharts/MarimekkoChartConstants.ts +112 -0
- package/src/grapher/stackedCharts/MarimekkoChartHelpers.ts +21 -0
- package/src/grapher/stackedCharts/MarimekkoChartState.ts +465 -0
- package/src/grapher/stackedCharts/MarimekkoChartThumbnail.tsx +168 -0
- package/src/grapher/stackedCharts/MarimekkoInternalLabels.tsx +124 -0
- package/src/grapher/stackedCharts/StackedAreaChart.tsx +678 -0
- package/src/grapher/stackedCharts/StackedAreaChartState.ts +34 -0
- package/src/grapher/stackedCharts/StackedAreaChartThumbnail.tsx +215 -0
- package/src/grapher/stackedCharts/StackedAreas.tsx +223 -0
- package/src/grapher/stackedCharts/StackedBarChart.tsx +619 -0
- package/src/grapher/stackedCharts/StackedBarChartState.ts +80 -0
- package/src/grapher/stackedCharts/StackedBarChartThumbnail.tsx +220 -0
- package/src/grapher/stackedCharts/StackedBarSegment.tsx +87 -0
- package/src/grapher/stackedCharts/StackedBars.tsx +102 -0
- package/src/grapher/stackedCharts/StackedConstants.ts +109 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChart.tsx +270 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChartState.ts +296 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChartThumbnail.tsx +27 -0
- package/src/grapher/stackedCharts/StackedDiscreteBars.tsx +648 -0
- package/src/grapher/stackedCharts/StackedUtils.ts +142 -0
- package/src/grapher/tabs/Tabs.scss +169 -0
- package/src/grapher/tabs/Tabs.tsx +54 -0
- package/src/grapher/tabs/TabsWithDropdown.scss +62 -0
- package/src/grapher/tabs/TabsWithDropdown.tsx +114 -0
- package/src/grapher/testData/OwidTestData.sample.ts +273 -0
- package/src/grapher/testData/OwidTestData.ts +64 -0
- package/src/grapher/timeline/TimelineComponent.scss +139 -0
- package/src/grapher/timeline/TimelineComponent.tsx +658 -0
- package/src/grapher/timeline/TimelineController.ts +368 -0
- package/src/grapher/timeline/readme.md +7 -0
- package/src/grapher/tooltip/Tooltip.scss +510 -0
- package/src/grapher/tooltip/Tooltip.tsx +294 -0
- package/src/grapher/tooltip/TooltipContents.tsx +383 -0
- package/src/grapher/tooltip/TooltipProps.ts +123 -0
- package/src/grapher/tooltip/TooltipState.ts +81 -0
- package/src/grapher/verticalLabels/VerticalLabels.tsx +31 -0
- package/src/grapher/verticalLabels/VerticalLabelsState.ts +154 -0
- package/src/index.ts +226 -0
- package/src/styles/charts.scss +15 -0
- package/src/types/NominalType.ts +30 -0
- package/src/types/OwidOrigin.ts +18 -0
- package/src/types/OwidSource.ts +9 -0
- package/src/types/OwidVariable.ts +133 -0
- package/src/types/OwidVariableDisplayConfigInterface.ts +49 -0
- package/src/types/analyticsTypes.ts +54 -0
- package/src/types/dbTypes/Tags.ts +11 -0
- package/src/types/domainTypes/Archive.ts +139 -0
- package/src/types/domainTypes/Author.ts +28 -0
- package/src/types/domainTypes/ContentGraph.ts +76 -0
- package/src/types/domainTypes/CoreTableTypes.ts +305 -0
- package/src/types/domainTypes/DeployStatus.ts +23 -0
- package/src/types/domainTypes/Layout.ts +34 -0
- package/src/types/domainTypes/Posts.ts +34 -0
- package/src/types/domainTypes/Search.ts +299 -0
- package/src/types/domainTypes/Site.ts +8 -0
- package/src/types/domainTypes/StaticViz.ts +64 -0
- package/src/types/domainTypes/Toc.ts +11 -0
- package/src/types/domainTypes/Tombstone.ts +19 -0
- package/src/types/domainTypes/Various.ts +79 -0
- package/src/types/gdocTypes/Gdoc.ts +280 -0
- package/src/types/grapherTypes/BinningStrategyTypes.ts +46 -0
- package/src/types/grapherTypes/GrapherConstants.ts +53 -0
- package/src/types/grapherTypes/GrapherTypes.ts +743 -0
- package/src/types/index.ts +316 -0
- package/src/types/wordpressTypes/WordpressTypes.ts +9 -0
- package/src/utils/Bounds.ts +439 -0
- package/src/utils/BrowserUtils.ts +12 -0
- package/src/utils/FuzzySearch.ts +74 -0
- package/src/utils/MultiDimDataPageConfig.ts +31 -0
- package/src/utils/OwidVariable.ts +82 -0
- package/src/utils/PointVector.ts +97 -0
- package/src/utils/PromiseCache.ts +36 -0
- package/src/utils/PromiseSwitcher.ts +52 -0
- package/src/utils/TimeBounds.ts +130 -0
- package/src/utils/Tippy.tsx +57 -0
- package/src/utils/Util.ts +2369 -0
- package/src/utils/archival/archivalDate.ts +48 -0
- package/src/utils/dayjs.ts +32 -0
- package/src/utils/formatValue.ts +242 -0
- package/src/utils/grapherConfigUtils.ts +81 -0
- package/src/utils/image.ts +225 -0
- package/src/utils/index.ts +318 -0
- package/src/utils/isPresent.ts +5 -0
- package/src/utils/metadataHelpers.ts +329 -0
- package/src/utils/persistable/Persistable.ts +82 -0
- package/src/utils/persistable/readme.md +50 -0
- package/src/utils/regions.json +5635 -0
- package/src/utils/regions.ts +463 -0
- package/src/utils/serializers.ts +16 -0
- package/src/utils/string.ts +42 -0
- package/src/utils/urls/Url.ts +195 -0
- package/src/utils/urls/UrlMigration.ts +10 -0
- package/src/utils/urls/UrlUtils.ts +54 -0
- package/src/utils/urls/readme.md +90 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
import * as _ from "lodash-es"
|
|
2
|
+
import { CSSProperties } from "react"
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { computed, makeObservable } from "mobx"
|
|
5
|
+
import {
|
|
6
|
+
excludeUndefined,
|
|
7
|
+
imemo,
|
|
8
|
+
Bounds,
|
|
9
|
+
FontFamily,
|
|
10
|
+
} from "../../utils/index.js"
|
|
11
|
+
import { DetailsMarker } from "../../types/index.js"
|
|
12
|
+
import { TextWrap } from "../TextWrap/TextWrap.js"
|
|
13
|
+
import { fromMarkdown } from "mdast-util-from-markdown"
|
|
14
|
+
import type { Content, Root } from "mdast"
|
|
15
|
+
import { match } from "ts-pattern"
|
|
16
|
+
import { urlRegex } from "../markdown/remarkPlainLinks.js"
|
|
17
|
+
import * as R from "remeda"
|
|
18
|
+
|
|
19
|
+
const SUPERSCRIPT_NUMERALS = {
|
|
20
|
+
"0": "\u2070",
|
|
21
|
+
"1": "\u00b9",
|
|
22
|
+
"2": "\u00b2",
|
|
23
|
+
"3": "\u00b3",
|
|
24
|
+
"4": "\u2074",
|
|
25
|
+
"5": "\u2075",
|
|
26
|
+
"6": "\u2076",
|
|
27
|
+
"7": "\u2077",
|
|
28
|
+
"8": "\u2078",
|
|
29
|
+
"9": "\u2079",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IRFontParams {
|
|
33
|
+
fontSize?: number
|
|
34
|
+
fontWeight?: number
|
|
35
|
+
fontFamily?: FontFamily
|
|
36
|
+
isItalic?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IRBreakpoint {
|
|
40
|
+
tokenIndex: number
|
|
41
|
+
tokenStartOffset: number
|
|
42
|
+
breakOffset: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IRToken {
|
|
46
|
+
width: number
|
|
47
|
+
getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined
|
|
48
|
+
toHTML(key?: React.Key): React.ReactElement | undefined
|
|
49
|
+
toSVG(key?: React.Key): React.ReactElement | undefined
|
|
50
|
+
toPlaintext(): string | undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class IRText implements IRToken {
|
|
54
|
+
constructor(
|
|
55
|
+
public text: string,
|
|
56
|
+
public fontParams?: IRFontParams
|
|
57
|
+
) {}
|
|
58
|
+
@imemo get width(): number {
|
|
59
|
+
return Bounds.forText(this.text, this.fontParams).width
|
|
60
|
+
}
|
|
61
|
+
@imemo get height(): number {
|
|
62
|
+
return this.fontParams?.fontSize || 13
|
|
63
|
+
}
|
|
64
|
+
getBreakpointBefore(): undefined {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
68
|
+
return <span key={key}>{this.text}</span>
|
|
69
|
+
}
|
|
70
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
71
|
+
return <React.Fragment key={key}>{this.text}</React.Fragment>
|
|
72
|
+
}
|
|
73
|
+
toPlaintext(): string {
|
|
74
|
+
return this.text
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class IRWhitespace implements IRToken {
|
|
79
|
+
constructor(public fontParams?: IRFontParams) {}
|
|
80
|
+
@imemo get width(): number {
|
|
81
|
+
return Bounds.forText(" ", this.fontParams).width
|
|
82
|
+
}
|
|
83
|
+
getBreakpointBefore(): IRBreakpoint {
|
|
84
|
+
// Have to give it some `breakOffset` because we designate locations
|
|
85
|
+
// to split based on it, and `0` leads to being exactly in between tokens.
|
|
86
|
+
return { tokenIndex: 0, tokenStartOffset: 0, breakOffset: 0.0001 }
|
|
87
|
+
}
|
|
88
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
89
|
+
return <span key={key}> </span>
|
|
90
|
+
}
|
|
91
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
92
|
+
return <React.Fragment key={key}> </React.Fragment>
|
|
93
|
+
}
|
|
94
|
+
toPlaintext(): string {
|
|
95
|
+
return " "
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class IRLineBreak implements IRToken {
|
|
100
|
+
get width(): number {
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
getBreakpointBefore(): undefined {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
107
|
+
return <br key={key} />
|
|
108
|
+
}
|
|
109
|
+
toSVG(): undefined {
|
|
110
|
+
// We have to deal with this special case in
|
|
111
|
+
// whatever procedure does text reflow.
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
toPlaintext(): string {
|
|
115
|
+
return "\n"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export abstract class IRElement implements IRToken {
|
|
120
|
+
constructor(
|
|
121
|
+
public children: IRToken[],
|
|
122
|
+
public fontParams?: IRFontParams
|
|
123
|
+
) {}
|
|
124
|
+
|
|
125
|
+
@imemo get width(): number {
|
|
126
|
+
return getLineWidth(this.children)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined {
|
|
130
|
+
return getBreakpointBefore(this.children, targetWidth)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
splitBefore(maxWidth: number): {
|
|
134
|
+
before: IRToken | undefined
|
|
135
|
+
after: IRToken | undefined
|
|
136
|
+
} {
|
|
137
|
+
const { before, after } = splitLineAtBreakpoint(this.children, maxWidth)
|
|
138
|
+
return {
|
|
139
|
+
// do not create tokens without children
|
|
140
|
+
before: before.length ? this.getClone(before) : undefined,
|
|
141
|
+
after: after.length ? this.getClone(after) : undefined,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
splitOnLineBreaks(): IRToken[][] {
|
|
146
|
+
const lines = splitAllOnNewline(this.children)
|
|
147
|
+
if (lines.length > 1) {
|
|
148
|
+
return lines.map((tokens) =>
|
|
149
|
+
// Do not create a clone without children.
|
|
150
|
+
// There aren't any children in a line when the first or last
|
|
151
|
+
// token is a newline.
|
|
152
|
+
tokens.length ? [this.getClone(tokens)] : []
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
// Do not create copies of element
|
|
156
|
+
// if there are no newlines inside.
|
|
157
|
+
return [[this]]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
abstract getClone(children: IRToken[]): IRElement
|
|
161
|
+
abstract toHTML(key?: React.Key): React.ReactElement
|
|
162
|
+
abstract toSVG(key?: React.Key): React.ReactElement
|
|
163
|
+
|
|
164
|
+
toPlaintext(): string {
|
|
165
|
+
return lineToPlaintext(this.children)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class IRBold extends IRElement {
|
|
170
|
+
getClone(children: IRToken[]): IRBold {
|
|
171
|
+
return new IRBold(children, this.fontParams)
|
|
172
|
+
}
|
|
173
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
174
|
+
return (
|
|
175
|
+
<strong key={key}>
|
|
176
|
+
{this.children.map((child, i) => child.toHTML(i))}
|
|
177
|
+
</strong>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
181
|
+
return (
|
|
182
|
+
<tspan key={key} style={{ fontWeight: 700 }}>
|
|
183
|
+
{this.children.map((child, i) => child.toSVG(i))}
|
|
184
|
+
</tspan>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export class IRSpan extends IRElement {
|
|
190
|
+
getClone(children: IRToken[]): IRSpan {
|
|
191
|
+
return new IRSpan(children, this.fontParams)
|
|
192
|
+
}
|
|
193
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
194
|
+
return (
|
|
195
|
+
<span key={key}>
|
|
196
|
+
{this.children.map((child, i) => child.toHTML(i))}
|
|
197
|
+
</span>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
201
|
+
return (
|
|
202
|
+
<tspan key={key}>
|
|
203
|
+
{this.children.map((child, i) => child.toSVG(i))}
|
|
204
|
+
</tspan>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export class IRSuperscript implements IRToken {
|
|
210
|
+
constructor(
|
|
211
|
+
public text: string,
|
|
212
|
+
public fontParams?: IRFontParams
|
|
213
|
+
) {}
|
|
214
|
+
@imemo get width(): number {
|
|
215
|
+
return Bounds.forText(this.text, { fontSize: this.height / 2 }).width
|
|
216
|
+
}
|
|
217
|
+
@imemo get height(): number {
|
|
218
|
+
return this.fontParams?.fontSize || 16
|
|
219
|
+
}
|
|
220
|
+
getBreakpointBefore(): undefined {
|
|
221
|
+
return undefined
|
|
222
|
+
}
|
|
223
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
224
|
+
return <sup key={key}>{this.text}</sup>
|
|
225
|
+
}
|
|
226
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
227
|
+
// replace numerals with literals, for everything else let the font-feature handle it
|
|
228
|
+
const style = { fontFeatureSettings: '"sups"' }
|
|
229
|
+
const text = this.text.replace(/./g, (c) =>
|
|
230
|
+
_.get(SUPERSCRIPT_NUMERALS, c, c)
|
|
231
|
+
)
|
|
232
|
+
return (
|
|
233
|
+
<React.Fragment key={key}>
|
|
234
|
+
<tspan style={style}>{text}</tspan>
|
|
235
|
+
</React.Fragment>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
toPlaintext(): string {
|
|
239
|
+
return this.text
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export class IRItalic extends IRElement {
|
|
244
|
+
getClone(children: IRToken[]): IRItalic {
|
|
245
|
+
return new IRItalic(children, this.fontParams)
|
|
246
|
+
}
|
|
247
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
248
|
+
return (
|
|
249
|
+
<em key={key}>
|
|
250
|
+
{this.children.map((child, i) => child.toHTML(i))}
|
|
251
|
+
</em>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
255
|
+
return (
|
|
256
|
+
<tspan key={key} style={{ fontStyle: "italic" }}>
|
|
257
|
+
{this.children.map((child, i) => child.toSVG(i))}
|
|
258
|
+
</tspan>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class IRLink extends IRElement {
|
|
264
|
+
constructor(
|
|
265
|
+
public href: string,
|
|
266
|
+
children: IRToken[],
|
|
267
|
+
fontParams?: IRFontParams
|
|
268
|
+
) {
|
|
269
|
+
super(children, fontParams)
|
|
270
|
+
}
|
|
271
|
+
getClone(children: IRToken[]): IRLink {
|
|
272
|
+
return new IRLink(this.href, children, this.fontParams)
|
|
273
|
+
}
|
|
274
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
275
|
+
return (
|
|
276
|
+
<a key={key} href={this.href}>
|
|
277
|
+
{this.children.map((child, i) => child.toHTML(i))}
|
|
278
|
+
</a>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
282
|
+
return (
|
|
283
|
+
<a
|
|
284
|
+
key={key}
|
|
285
|
+
href={this.href}
|
|
286
|
+
style={{ textDecoration: "underline" }}
|
|
287
|
+
>
|
|
288
|
+
{this.children.map((child, i) => child.toSVG(i))}
|
|
289
|
+
</a>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class IRDetailOnDemand extends IRElement {
|
|
295
|
+
constructor(
|
|
296
|
+
public term: string,
|
|
297
|
+
children: IRToken[],
|
|
298
|
+
fontParams?: IRFontParams
|
|
299
|
+
) {
|
|
300
|
+
super(children, fontParams)
|
|
301
|
+
}
|
|
302
|
+
getClone(children: IRToken[]): IRDetailOnDemand {
|
|
303
|
+
return new IRDetailOnDemand(this.term, children, this.fontParams)
|
|
304
|
+
}
|
|
305
|
+
toHTML(key?: React.Key): React.ReactElement {
|
|
306
|
+
return (
|
|
307
|
+
<span
|
|
308
|
+
key={key}
|
|
309
|
+
className="dod-span"
|
|
310
|
+
data-id={this.term}
|
|
311
|
+
tabIndex={0}
|
|
312
|
+
>
|
|
313
|
+
{this.children.map((child, i) => child.toHTML(i))}
|
|
314
|
+
</span>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
toSVG(key?: React.Key): React.ReactElement {
|
|
318
|
+
return (
|
|
319
|
+
<tspan key={key} className="dod-span" data-id={this.term}>
|
|
320
|
+
{this.children.map((child, i) => child.toSVG(i))}
|
|
321
|
+
</tspan>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function splitAllOnNewline(tokens: IRToken[]): IRToken[][] {
|
|
327
|
+
if (!tokens.length) return []
|
|
328
|
+
let currentLine: IRToken[] = []
|
|
329
|
+
const lines: IRToken[][] = [currentLine]
|
|
330
|
+
const unproccessed: IRToken[] = [...tokens]
|
|
331
|
+
while (unproccessed.length > 0) {
|
|
332
|
+
const token = unproccessed.shift()!
|
|
333
|
+
if (token instanceof IRElement) {
|
|
334
|
+
const [firstLine, ...otherLines] = token.splitOnLineBreaks()
|
|
335
|
+
if (firstLine) currentLine.push(...firstLine)
|
|
336
|
+
if (otherLines.length) {
|
|
337
|
+
lines.push(...otherLines)
|
|
338
|
+
currentLine = R.last(lines)!
|
|
339
|
+
}
|
|
340
|
+
} else if (token instanceof IRLineBreak) {
|
|
341
|
+
currentLine = []
|
|
342
|
+
lines.push(currentLine)
|
|
343
|
+
} else {
|
|
344
|
+
currentLine.push(token)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return lines
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function splitLineAtBreakpoint(
|
|
351
|
+
tokens: IRToken[],
|
|
352
|
+
breakWidth: number
|
|
353
|
+
): { before: IRToken[]; after: IRToken[] } {
|
|
354
|
+
let i = 0
|
|
355
|
+
let offset = 0
|
|
356
|
+
// finding the token where the split should be
|
|
357
|
+
// NOTE: the token may not be splittable, which is why we need an exact
|
|
358
|
+
// `breakWidth` provided, not the line width.
|
|
359
|
+
while (i < tokens.length - 1 && offset + tokens[i].width < breakWidth) {
|
|
360
|
+
offset += tokens[i].width
|
|
361
|
+
i++
|
|
362
|
+
}
|
|
363
|
+
const token = tokens[i]
|
|
364
|
+
if (token instanceof IRElement) {
|
|
365
|
+
const { before, after } = token.splitBefore(breakWidth - offset)
|
|
366
|
+
return {
|
|
367
|
+
before: excludeUndefined([...tokens.slice(0, i), before]),
|
|
368
|
+
after: excludeUndefined([after, ...tokens.slice(i + 1)]),
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
return { before: tokens.slice(0, i), after: trimLeft(tokens.slice(i)) }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function trimLeft(tokens: IRToken[]): IRToken[] {
|
|
376
|
+
let i = 0
|
|
377
|
+
while (i < tokens.length && tokens[i] instanceof IRWhitespace) {
|
|
378
|
+
i++
|
|
379
|
+
}
|
|
380
|
+
return tokens.slice(i)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Even though it says "before", it may return a breakpoint after, because
|
|
384
|
+
// there is no earlier breakpoint in the line.
|
|
385
|
+
export function getBreakpointBefore(
|
|
386
|
+
tokens: IRToken[],
|
|
387
|
+
maxWidth: number
|
|
388
|
+
): IRBreakpoint | undefined {
|
|
389
|
+
let tokenStartOffset = 0
|
|
390
|
+
let prevBreakpoint: IRBreakpoint | undefined = undefined
|
|
391
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
392
|
+
const token = tokens[index]
|
|
393
|
+
const candidate = token.getBreakpointBefore(maxWidth - tokenStartOffset)
|
|
394
|
+
if (candidate !== undefined) {
|
|
395
|
+
if (
|
|
396
|
+
prevBreakpoint &&
|
|
397
|
+
candidate.breakOffset + tokenStartOffset > maxWidth
|
|
398
|
+
) {
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
prevBreakpoint = {
|
|
402
|
+
tokenStartOffset,
|
|
403
|
+
tokenIndex: index,
|
|
404
|
+
breakOffset: candidate.breakOffset + tokenStartOffset,
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
tokenStartOffset += token.width
|
|
408
|
+
}
|
|
409
|
+
return prevBreakpoint
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function getLineWidth(tokens: IRToken[]): number {
|
|
413
|
+
return _.sum(tokens.map((token) => token.width))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// useful for debugging
|
|
417
|
+
export function lineToPlaintext(tokens: IRToken[]): string {
|
|
418
|
+
return tokens.map((t) => t.toPlaintext()).join("")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export const isTextToken = (token: IRToken): token is IRText | IRWhitespace =>
|
|
422
|
+
token instanceof IRText || token instanceof IRWhitespace
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Merges adjacent text tokens, because the way we render text in React is otherwise
|
|
426
|
+
* not very compatible with Google Translate, breaking our site in weird ways when
|
|
427
|
+
* translated.
|
|
428
|
+
* This is to be run _just before_ rendering to HTML, because it loses some
|
|
429
|
+
* information and is not easily reversible.
|
|
430
|
+
* See also https://github.com/owid/owid-grapher/issues/1785
|
|
431
|
+
*/
|
|
432
|
+
export const recursiveMergeTextTokens = (
|
|
433
|
+
tokens: IRToken[],
|
|
434
|
+
fontParams?: IRFontParams
|
|
435
|
+
): IRToken[] => {
|
|
436
|
+
if (tokens.length === 0) return []
|
|
437
|
+
|
|
438
|
+
// merge adjacent text tokens into one
|
|
439
|
+
const mergedTextTokens: IRToken[] = tokens.reduce((acc, token) => {
|
|
440
|
+
if (isTextToken(token)) {
|
|
441
|
+
const l = R.last(acc)
|
|
442
|
+
if (l && isTextToken(l)) {
|
|
443
|
+
// replace last value in acc with merged text token
|
|
444
|
+
acc.pop()
|
|
445
|
+
return [
|
|
446
|
+
...acc,
|
|
447
|
+
new IRText(
|
|
448
|
+
l.toPlaintext() + token.toPlaintext(),
|
|
449
|
+
fontParams
|
|
450
|
+
),
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return [...acc, token]
|
|
455
|
+
}, [] as IRToken[])
|
|
456
|
+
|
|
457
|
+
// recursively enter non-text tokens, and merge their children
|
|
458
|
+
return mergedTextTokens.map((token) => {
|
|
459
|
+
if (token instanceof IRElement) {
|
|
460
|
+
return token.getClone(
|
|
461
|
+
recursiveMergeTextTokens(token.children, fontParams)
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
return token
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function splitIntoLines(
|
|
469
|
+
tokens: IRToken[],
|
|
470
|
+
maxWidth: number
|
|
471
|
+
): IRToken[][] {
|
|
472
|
+
const processedLines: IRToken[][] = []
|
|
473
|
+
const unprocessedLines: IRToken[][] = splitAllOnNewline(tokens)
|
|
474
|
+
while (unprocessedLines.length) {
|
|
475
|
+
const currentLine = unprocessedLines.shift()!
|
|
476
|
+
if (getLineWidth(currentLine) <= maxWidth) {
|
|
477
|
+
processedLines.push(currentLine)
|
|
478
|
+
} else {
|
|
479
|
+
const breakpoint = getBreakpointBefore(currentLine, maxWidth)
|
|
480
|
+
if (!breakpoint) {
|
|
481
|
+
processedLines.push(currentLine)
|
|
482
|
+
} else {
|
|
483
|
+
const { before, after } = splitLineAtBreakpoint(
|
|
484
|
+
currentLine,
|
|
485
|
+
breakpoint.breakOffset
|
|
486
|
+
)
|
|
487
|
+
processedLines.push(before)
|
|
488
|
+
unprocessedLines.unshift(after)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return processedLines
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export const sumTextWrapHeights = (
|
|
496
|
+
elements: MarkdownTextWrap[] | TextWrap[],
|
|
497
|
+
spacer: number = 0
|
|
498
|
+
): number =>
|
|
499
|
+
_.sum(elements.map((element) => element.height)) +
|
|
500
|
+
(elements.length - 1) * spacer
|
|
501
|
+
|
|
502
|
+
type MarkdownTextWrapOptions = {
|
|
503
|
+
maxWidth?: number
|
|
504
|
+
fontFamily?: FontFamily
|
|
505
|
+
fontSize: number
|
|
506
|
+
fontWeight?: number
|
|
507
|
+
lineHeight?: number
|
|
508
|
+
style?: CSSProperties
|
|
509
|
+
detailsOrderedByReference?: string[]
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
type MarkdownTextWrapProps = { text: string } & MarkdownTextWrapOptions
|
|
513
|
+
|
|
514
|
+
type TextFragment = { text: string; bold?: boolean }
|
|
515
|
+
|
|
516
|
+
export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
|
|
517
|
+
constructor(props: MarkdownTextWrapProps) {
|
|
518
|
+
super(props)
|
|
519
|
+
makeObservable(this)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
static fromFragments({
|
|
523
|
+
main,
|
|
524
|
+
secondary,
|
|
525
|
+
newLine = "continue-line",
|
|
526
|
+
textWrapProps,
|
|
527
|
+
}: {
|
|
528
|
+
main: TextFragment
|
|
529
|
+
secondary: TextFragment
|
|
530
|
+
newLine?: "continue-line" | "always" | "avoid-wrap"
|
|
531
|
+
textWrapProps: Omit<MarkdownTextWrapOptions, "fontWeight">
|
|
532
|
+
}) {
|
|
533
|
+
const mainMarkdownText = maybeBoldMarkdownText(main)
|
|
534
|
+
const secondaryMarkdownText = maybeBoldMarkdownText(secondary)
|
|
535
|
+
|
|
536
|
+
const combinedTextContinued = [
|
|
537
|
+
mainMarkdownText,
|
|
538
|
+
secondaryMarkdownText,
|
|
539
|
+
].join(" ")
|
|
540
|
+
const combinedTextNewLine = [
|
|
541
|
+
mainMarkdownText,
|
|
542
|
+
secondaryMarkdownText,
|
|
543
|
+
].join("\n")
|
|
544
|
+
|
|
545
|
+
if (newLine === "always") {
|
|
546
|
+
return new MarkdownTextWrap({
|
|
547
|
+
text: combinedTextNewLine,
|
|
548
|
+
...textWrapProps,
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (newLine === "continue-line") {
|
|
553
|
+
return new MarkdownTextWrap({
|
|
554
|
+
text: combinedTextContinued,
|
|
555
|
+
...textWrapProps,
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// if newLine is set to 'avoid-wrap', we first try to fit the secondary text
|
|
560
|
+
// on the same line as the main text. If it doesn't fit, we place it on a new line.
|
|
561
|
+
|
|
562
|
+
const mainTextWrap = new MarkdownTextWrap({ ...main, ...textWrapProps })
|
|
563
|
+
const secondaryTextWrap = new MarkdownTextWrap({
|
|
564
|
+
text: secondaryMarkdownText,
|
|
565
|
+
...textWrapProps,
|
|
566
|
+
maxWidth:
|
|
567
|
+
mainTextWrap.maxWidth -
|
|
568
|
+
mainTextWrap.lastLineWidth -
|
|
569
|
+
Bounds.forText(" ", textWrapProps).width -
|
|
570
|
+
10, // arbitrary wiggle room
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const secondaryTextFitsOnSameLine =
|
|
574
|
+
secondaryTextWrap.svgLines.length === 1
|
|
575
|
+
if (secondaryTextFitsOnSameLine) {
|
|
576
|
+
return new MarkdownTextWrap({
|
|
577
|
+
text: combinedTextContinued,
|
|
578
|
+
...textWrapProps,
|
|
579
|
+
})
|
|
580
|
+
} else {
|
|
581
|
+
return new MarkdownTextWrap({
|
|
582
|
+
text: combinedTextNewLine,
|
|
583
|
+
...textWrapProps,
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@computed get maxWidth(): number {
|
|
589
|
+
return this.props.maxWidth ?? Infinity
|
|
590
|
+
}
|
|
591
|
+
@computed get lineHeight(): number {
|
|
592
|
+
return this.props.lineHeight ?? 1.1
|
|
593
|
+
}
|
|
594
|
+
@computed get fontSize(): number {
|
|
595
|
+
return this.props.fontSize
|
|
596
|
+
}
|
|
597
|
+
@computed get fontParams(): IRFontParams {
|
|
598
|
+
return {
|
|
599
|
+
fontFamily: this.props.fontFamily,
|
|
600
|
+
fontSize: this.props.fontSize,
|
|
601
|
+
fontWeight: this.props.fontWeight,
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
@computed get text(): string {
|
|
605
|
+
// NOTE: ❗Here we deviate from the normal markdown spec. We replace \n with <SPACE><SPACE>\n to make sure that single \n are treated as
|
|
606
|
+
// actual line breaks but only if none of the other markdown line break rules apply.
|
|
607
|
+
// This is a bit different to how markdown usually works but we have a substantial
|
|
608
|
+
// amount of legacy charts that use newlines in this way and it seems that it is
|
|
609
|
+
// better to support this simple case than to do a data migration of many chart subtitles.
|
|
610
|
+
const baseText = this.props.text
|
|
611
|
+
// This replace is a bit funky - we want to make sure that single \n are treated as
|
|
612
|
+
// actual line breaks but only if none of the other markdown line break rules apply.
|
|
613
|
+
// These are:
|
|
614
|
+
// - \n\n is always a new paragraph
|
|
615
|
+
// - Two spaces before \n is a line break (this rule is not entirely checked as we only check for a single space)
|
|
616
|
+
// - A backslash before \n is a line break
|
|
617
|
+
// The code below normalizes all cases to <SPACE><SPACE>\n which will lead to them surviving the markdown parsing
|
|
618
|
+
let text = baseText.trim()
|
|
619
|
+
text = text.replaceAll("\n\n", "@@LINEBREAK@@")
|
|
620
|
+
text = text.replaceAll("\\\n", "@@LINEBREAK@@")
|
|
621
|
+
text = text.replaceAll(" \n", "@@LINEBREAK@@")
|
|
622
|
+
text = text.replaceAll("\n", " \n")
|
|
623
|
+
text = text.replaceAll("@@LINEBREAK@@", " \n")
|
|
624
|
+
return text
|
|
625
|
+
}
|
|
626
|
+
@computed get detailsOrderedByReference(): string[] {
|
|
627
|
+
return this.props.detailsOrderedByReference || []
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
@computed get plaintext(): string {
|
|
631
|
+
return this.htmlLines.map(lineToPlaintext).join("\n")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
@computed get tokensFromMarkdown(): IRToken[] {
|
|
635
|
+
const tokens = convertMarkdownToIRTokens(this.text, this.fontParams)
|
|
636
|
+
return tokens
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@computed get htmlLines(): IRToken[][] {
|
|
640
|
+
const tokens = this.tokensFromMarkdown
|
|
641
|
+
const lines = splitIntoLines(tokens, this.maxWidth)
|
|
642
|
+
return lines.map((line) =>
|
|
643
|
+
recursiveMergeTextTokens(line, this.fontParams)
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
@computed get svgLines(): IRToken[][] {
|
|
648
|
+
const tokens = this.tokensFromMarkdown
|
|
649
|
+
const lines = splitIntoLines(tokens, this.maxWidth)
|
|
650
|
+
return lines
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
@computed get svgLinesWithDodReferenceNumbers(): IRToken[][] {
|
|
654
|
+
const references = this.detailsOrderedByReference
|
|
655
|
+
const tokens = this.tokensFromMarkdown
|
|
656
|
+
const tokensWithReferenceNumbers = appendReferenceNumbers(
|
|
657
|
+
tokens,
|
|
658
|
+
references
|
|
659
|
+
)
|
|
660
|
+
return splitIntoLines(tokensWithReferenceNumbers, this.maxWidth)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
@computed get width(): number {
|
|
664
|
+
const { htmlLines } = this
|
|
665
|
+
const lineLengths = htmlLines.map((tokens) =>
|
|
666
|
+
_.sumBy(tokens, (token) => token.width)
|
|
667
|
+
)
|
|
668
|
+
return _.max(lineLengths) ?? 0
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@computed get singleLineHeight(): number {
|
|
672
|
+
return this.fontSize * this.lineHeight
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
@computed get lastLineWidth(): number {
|
|
676
|
+
return _.sumBy(R.last(this.htmlLines), (token) => token.width) ?? 0
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@computed get height(): number {
|
|
680
|
+
const { htmlLines } = this
|
|
681
|
+
if (htmlLines.length === 0) return 0
|
|
682
|
+
return htmlLines.length * this.singleLineHeight
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
@computed get style(): any {
|
|
686
|
+
return {
|
|
687
|
+
...this.fontParams,
|
|
688
|
+
...this.props.style,
|
|
689
|
+
lineHeight: this.lineHeight,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
renderHTML() {
|
|
694
|
+
const { htmlLines } = this
|
|
695
|
+
if (htmlLines.length === 0) return null
|
|
696
|
+
return (
|
|
697
|
+
<span style={this.style} className="markdown-text-wrap">
|
|
698
|
+
{htmlLines.map((line, i) => {
|
|
699
|
+
const plaintextLine = line
|
|
700
|
+
.map((token) => token.toPlaintext())
|
|
701
|
+
.join("")
|
|
702
|
+
return (
|
|
703
|
+
<MarkdownTextWrapLine
|
|
704
|
+
key={`${plaintextLine}-${i}`}
|
|
705
|
+
line={line}
|
|
706
|
+
/>
|
|
707
|
+
)
|
|
708
|
+
})}
|
|
709
|
+
</span>
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
renderSVG(
|
|
714
|
+
x: number,
|
|
715
|
+
y: number,
|
|
716
|
+
{
|
|
717
|
+
textProps,
|
|
718
|
+
detailsMarker = "superscript",
|
|
719
|
+
id,
|
|
720
|
+
}: {
|
|
721
|
+
textProps?: React.SVGProps<SVGTextElement>
|
|
722
|
+
detailsMarker?: DetailsMarker
|
|
723
|
+
id?: string
|
|
724
|
+
} = {}
|
|
725
|
+
) {
|
|
726
|
+
const { fontSize, lineHeight } = this
|
|
727
|
+
const lines =
|
|
728
|
+
detailsMarker === "superscript"
|
|
729
|
+
? this.svgLinesWithDodReferenceNumbers
|
|
730
|
+
: this.svgLines
|
|
731
|
+
if (lines.length === 0) return <></>
|
|
732
|
+
|
|
733
|
+
// Magic number set through experimentation.
|
|
734
|
+
// The HTML and SVG renderers need to position lines identically.
|
|
735
|
+
// This number was tweaked until the overlaid HTML and SVG outputs
|
|
736
|
+
// overlap.
|
|
737
|
+
const HEIGHT_CORRECTION_FACTOR = 0.74
|
|
738
|
+
|
|
739
|
+
const textHeight = fontSize * HEIGHT_CORRECTION_FACTOR
|
|
740
|
+
const containerHeight = lineHeight * fontSize
|
|
741
|
+
const yOffset =
|
|
742
|
+
y + (containerHeight - (containerHeight - textHeight) / 2)
|
|
743
|
+
|
|
744
|
+
const getLineY = (lineIndex: number) =>
|
|
745
|
+
yOffset + lineHeight * fontSize * lineIndex
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
<g id={id} className="markdown-text-wrap">
|
|
749
|
+
<text
|
|
750
|
+
x={x.toFixed(1)}
|
|
751
|
+
y={yOffset.toFixed(1)}
|
|
752
|
+
style={this.style}
|
|
753
|
+
{...textProps}
|
|
754
|
+
>
|
|
755
|
+
{lines.map((line, lineIndex) => (
|
|
756
|
+
<tspan
|
|
757
|
+
key={lineIndex}
|
|
758
|
+
x={x}
|
|
759
|
+
y={getLineY(lineIndex).toFixed(1)}
|
|
760
|
+
>
|
|
761
|
+
{line.map((token, tokenIndex) =>
|
|
762
|
+
token.toSVG(tokenIndex)
|
|
763
|
+
)}
|
|
764
|
+
</tspan>
|
|
765
|
+
))}
|
|
766
|
+
</text>
|
|
767
|
+
{/* SVG doesn't support dotted underlines, so we draw them manually */}
|
|
768
|
+
{detailsMarker === "underline" &&
|
|
769
|
+
lines.map((line, lineIndex) => {
|
|
770
|
+
const y = (getLineY(lineIndex) + 2).toFixed(1)
|
|
771
|
+
let currWidth = 0
|
|
772
|
+
return line.map((token) => {
|
|
773
|
+
const underline =
|
|
774
|
+
token instanceof IRDetailOnDemand ? (
|
|
775
|
+
<line
|
|
776
|
+
className="dod-underline"
|
|
777
|
+
x1={x + currWidth}
|
|
778
|
+
y1={y}
|
|
779
|
+
x2={x + currWidth + token.width}
|
|
780
|
+
y2={y}
|
|
781
|
+
stroke="currentColor"
|
|
782
|
+
strokeWidth={1}
|
|
783
|
+
strokeDasharray={1}
|
|
784
|
+
// important for rotated text
|
|
785
|
+
transform={textProps?.transform}
|
|
786
|
+
/>
|
|
787
|
+
) : null
|
|
788
|
+
currWidth += token.width
|
|
789
|
+
return underline
|
|
790
|
+
})
|
|
791
|
+
})}
|
|
792
|
+
</g>
|
|
793
|
+
)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// An alias method that allows MarkdownTextWrap to be
|
|
797
|
+
// instantiated via JSX for HTML rendering
|
|
798
|
+
// <MarkdownTextWrap ... />
|
|
799
|
+
override render(): React.ReactElement | null {
|
|
800
|
+
return this.renderHTML()
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// The rule doesn't support class components in the same file.
|
|
805
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
806
|
+
function MarkdownTextWrapLine({
|
|
807
|
+
line,
|
|
808
|
+
}: {
|
|
809
|
+
line: IRToken[]
|
|
810
|
+
}): React.ReactElement {
|
|
811
|
+
return (
|
|
812
|
+
<span className="markdown-text-wrap__line">
|
|
813
|
+
{line.length ? line.map((token, i) => token.toHTML(i)) : <br />}
|
|
814
|
+
</span>
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export function convertMarkdownToIRTokens(
|
|
819
|
+
markdown: string,
|
|
820
|
+
fontParams?: IRFontParams
|
|
821
|
+
): IRToken[] {
|
|
822
|
+
const ast: Root = fromMarkdown(markdown)
|
|
823
|
+
const children = ast.children.flatMap((item) =>
|
|
824
|
+
convertMarkdownNodeToIRTokens(item, fontParams)
|
|
825
|
+
)
|
|
826
|
+
// ensure that there are no leading or trailing line breaks
|
|
827
|
+
return R.dropLastWhile(
|
|
828
|
+
R.dropWhile(children, (token) => token instanceof IRLineBreak),
|
|
829
|
+
(token) => token instanceof IRLineBreak
|
|
830
|
+
)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// When using mdast types version 4 this should be typed as:
|
|
834
|
+
// node: RootContentMap[keyof RootContentMap]
|
|
835
|
+
function convertMarkdownNodeToIRTokens(
|
|
836
|
+
node: Content,
|
|
837
|
+
fontParams: IRFontParams = {}
|
|
838
|
+
): IRToken[] {
|
|
839
|
+
const converted = match(node)
|
|
840
|
+
.with(
|
|
841
|
+
{
|
|
842
|
+
type: "blockquote",
|
|
843
|
+
},
|
|
844
|
+
(item) => {
|
|
845
|
+
return item.children.flatMap((child) =>
|
|
846
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
847
|
+
)
|
|
848
|
+
}
|
|
849
|
+
)
|
|
850
|
+
.with(
|
|
851
|
+
{
|
|
852
|
+
type: "break",
|
|
853
|
+
},
|
|
854
|
+
(_) => {
|
|
855
|
+
return [new IRLineBreak()]
|
|
856
|
+
}
|
|
857
|
+
)
|
|
858
|
+
.with(
|
|
859
|
+
{
|
|
860
|
+
type: "code",
|
|
861
|
+
},
|
|
862
|
+
(item) => {
|
|
863
|
+
return [new IRText(item.value, fontParams)]
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
.with(
|
|
867
|
+
{
|
|
868
|
+
type: "emphasis",
|
|
869
|
+
},
|
|
870
|
+
(item) => {
|
|
871
|
+
return [
|
|
872
|
+
new IRItalic(
|
|
873
|
+
item.children.flatMap((child) =>
|
|
874
|
+
convertMarkdownNodeToIRTokens(child, {
|
|
875
|
+
...fontParams,
|
|
876
|
+
isItalic: true,
|
|
877
|
+
})
|
|
878
|
+
)
|
|
879
|
+
),
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
)
|
|
883
|
+
.with(
|
|
884
|
+
{
|
|
885
|
+
type: "heading",
|
|
886
|
+
},
|
|
887
|
+
(item) => {
|
|
888
|
+
return item.children.flatMap((child) =>
|
|
889
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
.with(
|
|
894
|
+
{
|
|
895
|
+
type: "html",
|
|
896
|
+
},
|
|
897
|
+
(item) => {
|
|
898
|
+
return [new IRText(item.value, fontParams)]
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
.with(
|
|
902
|
+
{
|
|
903
|
+
type: "image",
|
|
904
|
+
},
|
|
905
|
+
(item) => {
|
|
906
|
+
return [new IRText(item.alt ?? "", fontParams)]
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
.with(
|
|
910
|
+
{
|
|
911
|
+
type: "inlineCode",
|
|
912
|
+
},
|
|
913
|
+
(item) => {
|
|
914
|
+
return [new IRText(item.value, fontParams)]
|
|
915
|
+
}
|
|
916
|
+
)
|
|
917
|
+
.with(
|
|
918
|
+
{
|
|
919
|
+
type: "link",
|
|
920
|
+
},
|
|
921
|
+
(item) => {
|
|
922
|
+
if (item.url.startsWith("#dod:")) {
|
|
923
|
+
const term = item.url.replace("#dod:", "")
|
|
924
|
+
return [
|
|
925
|
+
new IRDetailOnDemand(
|
|
926
|
+
term,
|
|
927
|
+
item.children.flatMap((child) =>
|
|
928
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
929
|
+
),
|
|
930
|
+
fontParams
|
|
931
|
+
),
|
|
932
|
+
]
|
|
933
|
+
} else
|
|
934
|
+
return [
|
|
935
|
+
new IRLink(
|
|
936
|
+
item.url,
|
|
937
|
+
item.children.flatMap((child) =>
|
|
938
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
939
|
+
)
|
|
940
|
+
),
|
|
941
|
+
]
|
|
942
|
+
}
|
|
943
|
+
)
|
|
944
|
+
.with(
|
|
945
|
+
{
|
|
946
|
+
type: "list",
|
|
947
|
+
},
|
|
948
|
+
(item) => {
|
|
949
|
+
if (item.ordered)
|
|
950
|
+
return item.children.flatMap((child, index) => [
|
|
951
|
+
new IRLineBreak(),
|
|
952
|
+
new IRText(`${index + 1}) `, fontParams),
|
|
953
|
+
...convertMarkdownNodeToIRTokens(child, fontParams),
|
|
954
|
+
])
|
|
955
|
+
else
|
|
956
|
+
return item.children.flatMap((child) => [
|
|
957
|
+
new IRLineBreak(),
|
|
958
|
+
new IRText(`• `, fontParams),
|
|
959
|
+
...convertMarkdownNodeToIRTokens(child, fontParams),
|
|
960
|
+
])
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
.with(
|
|
964
|
+
{
|
|
965
|
+
type: "listItem",
|
|
966
|
+
},
|
|
967
|
+
(item) => {
|
|
968
|
+
return item.children.flatMap((child) =>
|
|
969
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
970
|
+
)
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
.with(
|
|
974
|
+
{
|
|
975
|
+
type: "paragraph",
|
|
976
|
+
},
|
|
977
|
+
(item) => {
|
|
978
|
+
return item.children.flatMap((child) =>
|
|
979
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
)
|
|
983
|
+
.with(
|
|
984
|
+
{
|
|
985
|
+
type: "strong",
|
|
986
|
+
},
|
|
987
|
+
(item) => {
|
|
988
|
+
return [
|
|
989
|
+
new IRBold(
|
|
990
|
+
item.children.flatMap((child) =>
|
|
991
|
+
convertMarkdownNodeToIRTokens(child, {
|
|
992
|
+
...fontParams,
|
|
993
|
+
fontWeight: 700,
|
|
994
|
+
})
|
|
995
|
+
)
|
|
996
|
+
),
|
|
997
|
+
]
|
|
998
|
+
}
|
|
999
|
+
)
|
|
1000
|
+
.with(
|
|
1001
|
+
{
|
|
1002
|
+
type: "text",
|
|
1003
|
+
},
|
|
1004
|
+
(item) => {
|
|
1005
|
+
const splitted = item.value.split(/\s+/)
|
|
1006
|
+
const tokens = splitted.flatMap((text, i) => {
|
|
1007
|
+
const textNode = new IRText(text, fontParams)
|
|
1008
|
+
const node = text.match(urlRegex)
|
|
1009
|
+
? new IRLink(text, [textNode], fontParams)
|
|
1010
|
+
: textNode
|
|
1011
|
+
if (i < splitted.length - 1) {
|
|
1012
|
+
return [node, new IRWhitespace(fontParams)]
|
|
1013
|
+
} else return [node]
|
|
1014
|
+
})
|
|
1015
|
+
return tokens
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
.with(
|
|
1019
|
+
{
|
|
1020
|
+
type: "thematicBreak",
|
|
1021
|
+
},
|
|
1022
|
+
(_) => {
|
|
1023
|
+
return [new IRText("---", fontParams)]
|
|
1024
|
+
}
|
|
1025
|
+
)
|
|
1026
|
+
.with(
|
|
1027
|
+
{
|
|
1028
|
+
type: "delete",
|
|
1029
|
+
},
|
|
1030
|
+
(item) => {
|
|
1031
|
+
return item.children.flatMap((child) =>
|
|
1032
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
)
|
|
1036
|
+
// Now lets finish this with blocks for FootnoteDefinition, Definition, ImageReference, LinkReference, FootnoteReference, and Table
|
|
1037
|
+
.with(
|
|
1038
|
+
{
|
|
1039
|
+
type: "footnoteDefinition",
|
|
1040
|
+
},
|
|
1041
|
+
(item) => {
|
|
1042
|
+
return item.children.flatMap((child) =>
|
|
1043
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
1044
|
+
)
|
|
1045
|
+
}
|
|
1046
|
+
)
|
|
1047
|
+
.with(
|
|
1048
|
+
{
|
|
1049
|
+
type: "definition",
|
|
1050
|
+
},
|
|
1051
|
+
(item) => {
|
|
1052
|
+
return [
|
|
1053
|
+
new IRText(`${item.identifier}: ${item.label}`, fontParams),
|
|
1054
|
+
]
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
.with(
|
|
1058
|
+
{
|
|
1059
|
+
type: "imageReference",
|
|
1060
|
+
},
|
|
1061
|
+
(item) => {
|
|
1062
|
+
return [
|
|
1063
|
+
new IRText(`${item.identifier}: ${item.label}`, fontParams),
|
|
1064
|
+
]
|
|
1065
|
+
}
|
|
1066
|
+
)
|
|
1067
|
+
.with(
|
|
1068
|
+
{
|
|
1069
|
+
type: "linkReference",
|
|
1070
|
+
},
|
|
1071
|
+
(item) => {
|
|
1072
|
+
return [
|
|
1073
|
+
new IRText(`${item.identifier}: ${item.label}`, fontParams),
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
)
|
|
1077
|
+
.with(
|
|
1078
|
+
{
|
|
1079
|
+
type: "footnoteReference",
|
|
1080
|
+
},
|
|
1081
|
+
(item) => {
|
|
1082
|
+
return [
|
|
1083
|
+
new IRText(`${item.identifier}: ${item.label}`, fontParams),
|
|
1084
|
+
]
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
.with(
|
|
1088
|
+
{
|
|
1089
|
+
type: "table",
|
|
1090
|
+
},
|
|
1091
|
+
(item) => {
|
|
1092
|
+
return item.children.flatMap((child) =>
|
|
1093
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
.with(
|
|
1098
|
+
{
|
|
1099
|
+
type: "tableCell",
|
|
1100
|
+
},
|
|
1101
|
+
(item) => {
|
|
1102
|
+
return item.children.flatMap((child) =>
|
|
1103
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
// and now TableRow and Yaml
|
|
1108
|
+
.with(
|
|
1109
|
+
{
|
|
1110
|
+
type: "tableRow",
|
|
1111
|
+
},
|
|
1112
|
+
(item) => {
|
|
1113
|
+
return item.children.flatMap((child) =>
|
|
1114
|
+
convertMarkdownNodeToIRTokens(child, fontParams)
|
|
1115
|
+
)
|
|
1116
|
+
}
|
|
1117
|
+
)
|
|
1118
|
+
.with(
|
|
1119
|
+
{
|
|
1120
|
+
type: "yaml",
|
|
1121
|
+
},
|
|
1122
|
+
(item) => {
|
|
1123
|
+
return [new IRText(item.value, fontParams)]
|
|
1124
|
+
}
|
|
1125
|
+
)
|
|
1126
|
+
.exhaustive()
|
|
1127
|
+
return converted
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function appendReferenceNumbers(
|
|
1131
|
+
tokens: IRToken[],
|
|
1132
|
+
references: string[]
|
|
1133
|
+
): IRToken[] {
|
|
1134
|
+
function traverse(token: IRToken, callback: (token: IRToken) => any): any {
|
|
1135
|
+
if (token instanceof IRElement) {
|
|
1136
|
+
token.children.flatMap((child) => traverse(child, callback))
|
|
1137
|
+
}
|
|
1138
|
+
return callback(token)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const appendedTokens: IRToken[] = _.cloneDeep(tokens).flatMap((token) =>
|
|
1142
|
+
traverse(token, (token: IRToken) => {
|
|
1143
|
+
if (token instanceof IRDetailOnDemand) {
|
|
1144
|
+
const referenceIndex =
|
|
1145
|
+
references.findIndex((term) => term === token.term) + 1
|
|
1146
|
+
if (referenceIndex === 0) return token
|
|
1147
|
+
token.children.push(
|
|
1148
|
+
new IRSuperscript(String(referenceIndex), token.fontParams)
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
return token
|
|
1152
|
+
})
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
return appendedTokens
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function maybeBoldMarkdownText({
|
|
1159
|
+
text,
|
|
1160
|
+
bold,
|
|
1161
|
+
}: {
|
|
1162
|
+
text: string
|
|
1163
|
+
bold?: boolean
|
|
1164
|
+
}): string {
|
|
1165
|
+
return bold ? `**${text}**` : text
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export function toPlaintext(markdown: string): string {
|
|
1169
|
+
return new MarkdownTextWrap({
|
|
1170
|
+
text: markdown,
|
|
1171
|
+
fontSize: 10, // doesn't matter, but is a mandatory field
|
|
1172
|
+
}).plaintext
|
|
1173
|
+
}
|