Skip to content

Commit

Permalink
fix: only subscriptify chemical formulas that appear in text blocks (#65
Browse files Browse the repository at this point in the history
)

Fixes #62
  • Loading branch information
chrispcampbell authored Feb 24, 2024
1 parent 161b76e commit d9fe245
Show file tree
Hide file tree
Showing 7 changed files with 747 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ The following chemical formulas should be subscripted automatically:
* O2
* O3
* SF6

Those formulas should not be subscripted when they appear in URLs, for example [CO2](https://www.climateinteractive.org/CO2).
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"husky": "^8.0.1",
"npm-run-all": "^4.1.5",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.3.1"
},
"author": "Climate Interactive",
"license": "MIT",
Expand Down
5 changes: 4 additions & 1 deletion packages/docs-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
"prettier:fix": "prettier --write .",
"precommit": "run-s lint prettier:check",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "vitest run",
"build": "tsup-node",
"watch": "tsup-node --watch",
"ci:build": "run-s clean lint prettier:check type-check build"
"ci:build": "run-s clean lint prettier:check type-check test:ci build"
},
"dependencies": {
"@compodoc/live-server": "^1.2.3",
Expand Down
46 changes: 46 additions & 0 deletions packages/docs-builder/src/gen-html.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund. All rights reserved.

import { describe, expect, it } from 'vitest'

import { convertMarkdownToHtml, subscriptify } from './gen-html'

describe('subscriptify', () => {
it('should convert chemical formulas', () => {
expect(subscriptify('This is -CO2-')).toBe('This is -CO<sub>2</sub>-')
expect(subscriptify('This is -CF4-')).toBe('This is -CF<sub>4</sub>-')
expect(subscriptify('This is -CH4-')).toBe('This is -CH<sub>4</sub>-')
expect(subscriptify('This is -H2O-')).toBe('This is -H<sub>2</sub>O-')
expect(subscriptify('This is -N2O-')).toBe('This is -N<sub>2</sub>O-')
expect(subscriptify('This is -NF3-')).toBe('This is -NF<sub>3</sub>-')
expect(subscriptify('This is -O2-')).toBe('This is -O<sub>2</sub>-')
expect(subscriptify('This is -O3-')).toBe('This is -O<sub>3</sub>-')
expect(subscriptify('This is -SF6-')).toBe('This is -SF<sub>6</sub>-')
expect(subscriptify('This is CO2 and CH4 and C12H22O11')).toBe(
'This is CO<sub>2</sub> and CH<sub>4</sub> and C12H22O11'
)
})
})

describe('convertMarkdownToHtml', () => {
it('should convert chemical formulas inside text elements only', () => {
// Verify that transformations are applied when text appears in different
// kinds of elements
expect(convertMarkdownToHtml(undefined, 'This is -CO2-')).toBe(
'<p>This is -CO<sub>2</sub>-</p>\n'
)
expect(convertMarkdownToHtml(undefined, '# This is CO2')).toBe(
'<h1 id="this-is-co2">This is CO<sub>2</sub></h1>\n'
)
expect(convertMarkdownToHtml(undefined, '> This is _CO2_')).toBe(
'<blockquote>\n<p>This is <em>CO<sub>2</sub></em></p>\n</blockquote>\n'
)

// Verify that attributes are not transformed
expect(convertMarkdownToHtml(undefined, '[This is CO2](https://google.com/CO2)')).toBe(
'<p><a href="https://google.com/CO2">This is CO<sub>2</sub></a></p>\n'
)
expect(convertMarkdownToHtml(undefined, '<img src="Hist_CO2.jpg"/>')).toBe(
'<img src="Hist_CO2.jpg"/>'
)
})
})
148 changes: 79 additions & 69 deletions packages/docs-builder/src/gen-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,62 +124,8 @@ export function generateHtml(context: Context, mdRelPath: string, mdPage: Markdo
// Add the Markdown content to the search index
context.searchIndex.addMarkdownPage(md, htmlRelPath)

// Customize HTML generation
marked.use({
renderer: {
// Transform special `glossary:` definition links to include a tooltip
// and a link to the glossary page/definition
link: (href, title, text) => {
let classPart: string
let textPart: string
let hrefPart: string
const m = href.match(/glossary:(\w+)/)
if (m) {
// This is a glossary link; insert a tooltip element
const termKey = m[1]
const tooltipText = context.getBlockText(`glossary__${termKey}__def`)
if (tooltipText === undefined) {
throw new Error(
context.getScopedMessage(`No glossary definition found for key=${termKey}`)
)
}
const tooltipHtml = marked.parseInline(tooltipText).replace(/\n/g, '<br/>')
classPart = ' class="glossary-link"'
textPart = `${text}<span class="tooltip"><span class="tooltip-arrow"> </span>${tooltipHtml}</span>`
// TODO: This path assumes the page is in the `guide` directory; need to fix
// this if we include glossary links on the index page
hrefPart = ` href="./glossary.html#glossary__${termKey}"`
} else {
// This is a normal link
classPart = ''
textPart = text
hrefPart = href ? ` href="${href}"` : ''
}

const titlePart = title ? ` title="${title}"` : ''
return `<a${classPart}${hrefPart}${titlePart}>${subscriptify(textPart)}</a>`
},

// Wrap tables in a div to allow for responsive scrolling behavior
table: (header, body) => {
let classes = 'table-container'
if (mdRelPath.includes('tech_removal')) {
// XXX: Include a special class for the "CDR Methods" table on the Tech CDR page
// in the En-ROADS User Guide so that we can target it in CSS. Currently we
// check the number of rows to differentiate it from the other slider settings
// table on that page.
const rowTags = [...body.matchAll(/<tr>/g)]
if (rowTags.length > 1) {
classes += ' removal_methods'
}
}
return `<div class="${classes}"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`
}
}
})

// Parse the Markdown into HTML
const body = marked.parse(md)
// Convert the Markdown content to HTML
const body = convertMarkdownToHtml(context, md)

// Clear the current page
context.setCurrentPage(undefined)
Expand Down Expand Up @@ -237,9 +183,6 @@ export function writeHtmlFile(
// in a separate tab automatically
body = body.replace(/(href="http(.*?)")/g, 'target="_blank" rel="noopener noreferrer" $1')

// Convert substrings like "CO2" to subscripted form ("CO<sub>2</sub>")
body = subscriptify(body)

// Get the path to the logo image
let logoPath: string
if (context.config.logoPath?.length > 0 && templateName !== 'simple') {
Expand Down Expand Up @@ -559,6 +502,80 @@ const subscriptMap = new Map([
['SF6', 'SF<sub>6</sub>']
])

/**
* Convert the given Markdown content to HTML, applying custom conversions.
*
* @param context The language-specific context.
* @param md The Markdown content.
* @return The resulting HTML content.
*/
export function convertMarkdownToHtml(context: Context, md: string): string {
// Customize HTML generation
marked.use({
renderer: {
// Convert substrings like "CO2" to subscripted form ("CO<sub>2</sub>").
// Performing this conversion on text elements only ensures that we don't
// affect other things like URLs or image paths.
text: text => {
return subscriptify(text)
},

// Transform special `glossary:` definition links to include a tooltip
// and a link to the glossary page/definition
link: (href, title, text) => {
let classPart: string
let textPart: string
let hrefPart: string
const m = href.match(/glossary:(\w+)/)
if (m) {
// This is a glossary link; insert a tooltip element
const termKey = m[1]
const tooltipText = context.getBlockText(`glossary__${termKey}__def`)
if (tooltipText === undefined) {
throw new Error(
context.getScopedMessage(`No glossary definition found for key=${termKey}`)
)
}
const tooltipHtml = marked.parseInline(tooltipText).replace(/\n/g, '<br/>')
classPart = ' class="glossary-link"'
textPart = `${text}<span class="tooltip"><span class="tooltip-arrow"> </span>${tooltipHtml}</span>`
// TODO: This path assumes the page is in the `guide` directory; need to fix
// this if we include glossary links on the index page
hrefPart = ` href="./glossary.html#glossary__${termKey}"`
} else {
// This is a normal link
classPart = ''
textPart = text
hrefPart = href ? ` href="${href}"` : ''
}

const titlePart = title ? ` title="${title}"` : ''
return `<a${classPart}${hrefPart}${titlePart}>${subscriptify(textPart)}</a>`
},

// Wrap tables in a div to allow for responsive scrolling behavior
table: (header, body) => {
const mdRelPath = context.getCurrentPage()
let classes = 'table-container'
if (mdRelPath.includes('tech_removal')) {
// XXX: Include a special class for the "CDR Methods" table on the Tech CDR page
// in the En-ROADS User Guide so that we can target it in CSS. Currently we
// check the number of rows to differentiate it from the other slider settings
// table on that page.
const rowTags = [...body.matchAll(/<tr>/g)]
if (rowTags.length > 1) {
classes += ' removal_methods'
}
}
return `<div class="${classes}"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`
}
}
})

// Parse the Markdown into HTML
return marked.parse(md)
}

/**
* Replace non-subscript forms of common chemicals with their subscripted equivalent.
* For example, "CO2" will be converted to "CO<sub>2</sub>". This only handles
Expand All @@ -576,15 +593,8 @@ const subscriptMap = new Map([
* @param s The input string.
* @return A new string containing subscripted chemical names.
*/
function subscriptify(s: string): string {
// XXX: Some historical graph images in the En-ROADS User Guide have
// {CO2,CH4,N2O} in the file name, so this regex is set up to avoid
// converting those filenames
return s.replace(/(Hist_)?(CO2|CF4|CH4|H2O|N2O|NF3|O2|O3|SF6)/g, (m, m1, m2) => {
if (m1) {
return m
} else {
return subscriptMap.get(m2)
}
export function subscriptify(s: string): string {
return s.replace(/(CO2|CF4|CH4|H2O|N2O|NF3|O2|O3|SF6)/g, (_m, m1) => {
return subscriptMap.get(m1)
})
}
1 change: 1 addition & 0 deletions packages/docs-builder/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"esModuleInterop": true,
// Enable strict enforcement of `import type`
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
Expand Down
Loading

0 comments on commit d9fe245

Please sign in to comment.