All files / owid-grapher/baker formatting.tsx

69.74% Statements 106/152
92.31% Branches 12/13
75% Functions 6/8
69.74% Lines 106/152

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 1791x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x     4x 1x 1x 1x 1x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 4x 4x 4x 4x 4x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 4x 4x 1x 1x                                                                             1x 1x                                                                  
import * as cheerio from "cheerio"
import {
    DataValueConfiguration,
    DataValueQueryArgs,
    FormattedPost,
    FormattingOptions,
    KeyValueProps,
    OwidVariableId,
} from "../clientUtils/owidTypes"
import { Country } from "../clientUtils/countries"
import { countryProfileDefaultCountryPlaceholder } from "../site/countryProfileProjects"
import { BAKED_BASE_URL, WORDPRESS_URL } from "../settings/serverSettings"
import { DATA_VALUE } from "../site/DataValue"
import { OwidVariablesAndEntityKey } from "../clientUtils/OwidVariable"
import {
    OwidChartDimensionInterface,
    OwidVariableDisplayConfigInterface,
} from "../clientUtils/OwidVariableDisplayConfigInterface"
import { legacyToOwidTableAndDimensions } from "../grapher/core/LegacyToOwidTable"
import { getBodyHtml } from "../site/formatting"
 
export const DEEP_LINK_CLASS = "deep-link"
 
// Standardize urls
export const formatLinks = (html: string) =>
    html
        .replace(new RegExp(WORDPRESS_URL, "g"), BAKED_BASE_URL)
        .replace(new RegExp("https?://owid.cloud", "g"), BAKED_BASE_URL)
        .replace(new RegExp("https?://ourworldindata.org", "g"), BAKED_BASE_URL)
 
export const extractFormattingOptions = (html: string): FormattingOptions => {
    const formattingOptionsMatch = html.match(
        /<!--\s*formatting-options\s+(.*)\s*-->/
    )
    return formattingOptionsMatch
        ? parseFormattingOptions(formattingOptionsMatch[1])
        : {}
}
 
// Converts "toc:false raw somekey:somevalue" to { toc: false, raw: true, somekey: "somevalue" }
// If only the key is specified, the value is assumed to be true (e.g. "raw" above)
export const parseFormattingOptions = (text: string): FormattingOptions => {
    return parseKeyValueArgs(text)
}
 
export const dataValueRegex = new RegExp(
    `{{\\s*${DATA_VALUE}\\s*(.+?)\\s*}}`,
    "g"
)
 
export const extractDataValuesConfiguration = async (
    html: string
): Promise<Map<string, DataValueConfiguration>> => {
    const dataValueSeparator = /\s*\|\s*/
    const dataValuesConfigurations = new Map<string, DataValueConfiguration>()
 
    const dataValueMatches = html.matchAll(dataValueRegex)
    for (const match of dataValueMatches) {
        const dataValueConfigurationString = match[1]
        const [queryArgsString, template] =
            dataValueConfigurationString.split(dataValueSeparator)
        const queryArgs = parseDataValueArgs(queryArgsString)
 
        dataValuesConfigurations.set(dataValueConfigurationString, {
            queryArgs,
            template,
        })
    }
    return dataValuesConfigurations
}
 
export const parseDataValueArgs = (
    rawArgsString: string
): DataValueQueryArgs => {
    return Object.fromEntries(
        Object.entries(parseKeyValueArgs(rawArgsString)).map(([k, v]) => [
            k,
            Number(v),
        ])
    )
}
 
export const parseKeyValueArgs = (text: string): KeyValueProps => {
    const options: { [key: string]: string | boolean } = {}
    text.split(/\s+/)
        // filter out empty strings
        .filter((s) => s && s.length > 0)
        .forEach((option: string) => {
            // using regex instead of split(":") to handle ":" in value
            // e.g. {{LastUpdated timestampUrl:https://...}}
            const optionRegex = /([^:]+):?(.*)/
            const [, name, value] = option.match(optionRegex) as [
                any,
                string,
                string
            ]
            let parsedValue
            if (value === "" || value === "true") parsedValue = true
            else if (value === "false") parsedValue = false
            else parsedValue = value
            options[name] = parsedValue
        })
    return options
}
 
export const formatDataValue = (
    value: number,
    variableId: OwidVariableId,
    legacyVariableDisplayConfig: OwidVariableDisplayConfigInterface = {},
    legacyChartDimension: OwidChartDimensionInterface | undefined
) => {
    if (!legacyChartDimension) return
    const legacyVariableConfig: OwidVariablesAndEntityKey = {
        entityKey: {},
        variables: {
            [variableId]: {
                id: variableId,
                display: legacyVariableDisplayConfig,
                values: [value],
                years: [],
                entities: [],
            },
        },
    }

    const legacyGrapherConfig = {
        dimensions: [
            {
                ...legacyChartDimension,
            },
        ],
    }

    const { table, dimensions } = legacyToOwidTableAndDimensions(
        legacyVariableConfig,
        legacyGrapherConfig
    )

    const formattedValueWithUnit = table
        .get(dimensions[0].slug)
        .formatValueLong(table.rows[0][variableId])

    return formattedValueWithUnit
}
 
export const formatCountryProfile = (
    post: FormattedPost,
    country: Country
): FormattedPost => {
    // Localize country selector
    const htmlWithLocalizedCountrySelector = post.html.replace(
        countryProfileDefaultCountryPlaceholder,
        country.code
    )
 
    const cheerioEl = cheerio.load(htmlWithLocalizedCountrySelector)
 
    // Inject country names on h3 headings which have been already identified as subsections
    // (filtering them out based on whether they have a deep link anchor attached to them)
    cheerioEl(`h3 a.${DEEP_LINK_CLASS}`).each((_, deepLinkAnchor) => {
        const $deepLinkAnchor = cheerioEl(deepLinkAnchor)
        $deepLinkAnchor.after(`${country.name}: `)
    })
 
    return { ...post, html: getBodyHtml(cheerioEl) }
}
 
// Relies on formatLinks URL standardisation
export const isStandaloneInternalLink = (
    el: CheerioElement,
    $: CheerioStatic
) => {
    return (
        el.attribs.href?.startsWith(BAKED_BASE_URL) &&
        el.parent.tagName === "p" &&
        $(el.parent).contents().length === 1
    )
}