Skip to content

Commit d9fe245

Browse files
fix: only subscriptify chemical formulas that appear in text blocks (#65)
Fixes #62
1 parent 161b76e commit d9fe245

File tree

7 files changed

+747
-98
lines changed

7 files changed

+747
-98
lines changed

examples/sample-docs/projects/sample-guide/content/appendix.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ The following chemical formulas should be subscripted automatically:
1414
* O2
1515
* O3
1616
* SF6
17+
18+
Those formulas should not be subscripted when they appear in URLs, for example [CO2](https://www.climateinteractive.org/CO2).

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"husky": "^8.0.1",
2222
"npm-run-all": "^4.1.5",
2323
"tsup": "^8.0.2",
24-
"typescript": "^5.3.3"
24+
"typescript": "^5.3.3",
25+
"vitest": "^1.3.1"
2526
},
2627
"author": "Climate Interactive",
2728
"license": "MIT",

packages/docs-builder/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
"prettier:fix": "prettier --write .",
2727
"precommit": "run-s lint prettier:check",
2828
"type-check": "tsc --noEmit",
29+
"test": "vitest run",
30+
"test:watch": "vitest",
31+
"test:ci": "vitest run",
2932
"build": "tsup-node",
3033
"watch": "tsup-node --watch",
31-
"ci:build": "run-s clean lint prettier:check type-check build"
34+
"ci:build": "run-s clean lint prettier:check type-check test:ci build"
3235
},
3336
"dependencies": {
3437
"@compodoc/live-server": "^1.2.3",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2022 Climate Interactive / New Venture Fund. All rights reserved.
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { convertMarkdownToHtml, subscriptify } from './gen-html'
6+
7+
describe('subscriptify', () => {
8+
it('should convert chemical formulas', () => {
9+
expect(subscriptify('This is -CO2-')).toBe('This is -CO<sub>2</sub>-')
10+
expect(subscriptify('This is -CF4-')).toBe('This is -CF<sub>4</sub>-')
11+
expect(subscriptify('This is -CH4-')).toBe('This is -CH<sub>4</sub>-')
12+
expect(subscriptify('This is -H2O-')).toBe('This is -H<sub>2</sub>O-')
13+
expect(subscriptify('This is -N2O-')).toBe('This is -N<sub>2</sub>O-')
14+
expect(subscriptify('This is -NF3-')).toBe('This is -NF<sub>3</sub>-')
15+
expect(subscriptify('This is -O2-')).toBe('This is -O<sub>2</sub>-')
16+
expect(subscriptify('This is -O3-')).toBe('This is -O<sub>3</sub>-')
17+
expect(subscriptify('This is -SF6-')).toBe('This is -SF<sub>6</sub>-')
18+
expect(subscriptify('This is CO2 and CH4 and C12H22O11')).toBe(
19+
'This is CO<sub>2</sub> and CH<sub>4</sub> and C12H22O11'
20+
)
21+
})
22+
})
23+
24+
describe('convertMarkdownToHtml', () => {
25+
it('should convert chemical formulas inside text elements only', () => {
26+
// Verify that transformations are applied when text appears in different
27+
// kinds of elements
28+
expect(convertMarkdownToHtml(undefined, 'This is -CO2-')).toBe(
29+
'<p>This is -CO<sub>2</sub>-</p>\n'
30+
)
31+
expect(convertMarkdownToHtml(undefined, '# This is CO2')).toBe(
32+
'<h1 id="this-is-co2">This is CO<sub>2</sub></h1>\n'
33+
)
34+
expect(convertMarkdownToHtml(undefined, '> This is _CO2_')).toBe(
35+
'<blockquote>\n<p>This is <em>CO<sub>2</sub></em></p>\n</blockquote>\n'
36+
)
37+
38+
// Verify that attributes are not transformed
39+
expect(convertMarkdownToHtml(undefined, '[This is CO2](https://google.com/CO2)')).toBe(
40+
'<p><a href="https://google.com/CO2">This is CO<sub>2</sub></a></p>\n'
41+
)
42+
expect(convertMarkdownToHtml(undefined, '<img src="Hist_CO2.jpg"/>')).toBe(
43+
'<img src="Hist_CO2.jpg"/>'
44+
)
45+
})
46+
})

packages/docs-builder/src/gen-html.ts

Lines changed: 79 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -124,62 +124,8 @@ export function generateHtml(context: Context, mdRelPath: string, mdPage: Markdo
124124
// Add the Markdown content to the search index
125125
context.searchIndex.addMarkdownPage(md, htmlRelPath)
126126

127-
// Customize HTML generation
128-
marked.use({
129-
renderer: {
130-
// Transform special `glossary:` definition links to include a tooltip
131-
// and a link to the glossary page/definition
132-
link: (href, title, text) => {
133-
let classPart: string
134-
let textPart: string
135-
let hrefPart: string
136-
const m = href.match(/glossary:(\w+)/)
137-
if (m) {
138-
// This is a glossary link; insert a tooltip element
139-
const termKey = m[1]
140-
const tooltipText = context.getBlockText(`glossary__${termKey}__def`)
141-
if (tooltipText === undefined) {
142-
throw new Error(
143-
context.getScopedMessage(`No glossary definition found for key=${termKey}`)
144-
)
145-
}
146-
const tooltipHtml = marked.parseInline(tooltipText).replace(/\n/g, '<br/>')
147-
classPart = ' class="glossary-link"'
148-
textPart = `${text}<span class="tooltip"><span class="tooltip-arrow"> </span>${tooltipHtml}</span>`
149-
// TODO: This path assumes the page is in the `guide` directory; need to fix
150-
// this if we include glossary links on the index page
151-
hrefPart = ` href="./glossary.html#glossary__${termKey}"`
152-
} else {
153-
// This is a normal link
154-
classPart = ''
155-
textPart = text
156-
hrefPart = href ? ` href="${href}"` : ''
157-
}
158-
159-
const titlePart = title ? ` title="${title}"` : ''
160-
return `<a${classPart}${hrefPart}${titlePart}>${subscriptify(textPart)}</a>`
161-
},
162-
163-
// Wrap tables in a div to allow for responsive scrolling behavior
164-
table: (header, body) => {
165-
let classes = 'table-container'
166-
if (mdRelPath.includes('tech_removal')) {
167-
// XXX: Include a special class for the "CDR Methods" table on the Tech CDR page
168-
// in the En-ROADS User Guide so that we can target it in CSS. Currently we
169-
// check the number of rows to differentiate it from the other slider settings
170-
// table on that page.
171-
const rowTags = [...body.matchAll(/<tr>/g)]
172-
if (rowTags.length > 1) {
173-
classes += ' removal_methods'
174-
}
175-
}
176-
return `<div class="${classes}"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`
177-
}
178-
}
179-
})
180-
181-
// Parse the Markdown into HTML
182-
const body = marked.parse(md)
127+
// Convert the Markdown content to HTML
128+
const body = convertMarkdownToHtml(context, md)
183129

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

240-
// Convert substrings like "CO2" to subscripted form ("CO<sub>2</sub>")
241-
body = subscriptify(body)
242-
243186
// Get the path to the logo image
244187
let logoPath: string
245188
if (context.config.logoPath?.length > 0 && templateName !== 'simple') {
@@ -559,6 +502,80 @@ const subscriptMap = new Map([
559502
['SF6', 'SF<sub>6</sub>']
560503
])
561504

505+
/**
506+
* Convert the given Markdown content to HTML, applying custom conversions.
507+
*
508+
* @param context The language-specific context.
509+
* @param md The Markdown content.
510+
* @return The resulting HTML content.
511+
*/
512+
export function convertMarkdownToHtml(context: Context, md: string): string {
513+
// Customize HTML generation
514+
marked.use({
515+
renderer: {
516+
// Convert substrings like "CO2" to subscripted form ("CO<sub>2</sub>").
517+
// Performing this conversion on text elements only ensures that we don't
518+
// affect other things like URLs or image paths.
519+
text: text => {
520+
return subscriptify(text)
521+
},
522+
523+
// Transform special `glossary:` definition links to include a tooltip
524+
// and a link to the glossary page/definition
525+
link: (href, title, text) => {
526+
let classPart: string
527+
let textPart: string
528+
let hrefPart: string
529+
const m = href.match(/glossary:(\w+)/)
530+
if (m) {
531+
// This is a glossary link; insert a tooltip element
532+
const termKey = m[1]
533+
const tooltipText = context.getBlockText(`glossary__${termKey}__def`)
534+
if (tooltipText === undefined) {
535+
throw new Error(
536+
context.getScopedMessage(`No glossary definition found for key=${termKey}`)
537+
)
538+
}
539+
const tooltipHtml = marked.parseInline(tooltipText).replace(/\n/g, '<br/>')
540+
classPart = ' class="glossary-link"'
541+
textPart = `${text}<span class="tooltip"><span class="tooltip-arrow"> </span>${tooltipHtml}</span>`
542+
// TODO: This path assumes the page is in the `guide` directory; need to fix
543+
// this if we include glossary links on the index page
544+
hrefPart = ` href="./glossary.html#glossary__${termKey}"`
545+
} else {
546+
// This is a normal link
547+
classPart = ''
548+
textPart = text
549+
hrefPart = href ? ` href="${href}"` : ''
550+
}
551+
552+
const titlePart = title ? ` title="${title}"` : ''
553+
return `<a${classPart}${hrefPart}${titlePart}>${subscriptify(textPart)}</a>`
554+
},
555+
556+
// Wrap tables in a div to allow for responsive scrolling behavior
557+
table: (header, body) => {
558+
const mdRelPath = context.getCurrentPage()
559+
let classes = 'table-container'
560+
if (mdRelPath.includes('tech_removal')) {
561+
// XXX: Include a special class for the "CDR Methods" table on the Tech CDR page
562+
// in the En-ROADS User Guide so that we can target it in CSS. Currently we
563+
// check the number of rows to differentiate it from the other slider settings
564+
// table on that page.
565+
const rowTags = [...body.matchAll(/<tr>/g)]
566+
if (rowTags.length > 1) {
567+
classes += ' removal_methods'
568+
}
569+
}
570+
return `<div class="${classes}"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`
571+
}
572+
}
573+
})
574+
575+
// Parse the Markdown into HTML
576+
return marked.parse(md)
577+
}
578+
562579
/**
563580
* Replace non-subscript forms of common chemicals with their subscripted equivalent.
564581
* For example, "CO2" will be converted to "CO<sub>2</sub>". This only handles
@@ -576,15 +593,8 @@ const subscriptMap = new Map([
576593
* @param s The input string.
577594
* @return A new string containing subscripted chemical names.
578595
*/
579-
function subscriptify(s: string): string {
580-
// XXX: Some historical graph images in the En-ROADS User Guide have
581-
// {CO2,CH4,N2O} in the file name, so this regex is set up to avoid
582-
// converting those filenames
583-
return s.replace(/(Hist_)?(CO2|CF4|CH4|H2O|N2O|NF3|O2|O3|SF6)/g, (m, m1, m2) => {
584-
if (m1) {
585-
return m
586-
} else {
587-
return subscriptMap.get(m2)
588-
}
596+
export function subscriptify(s: string): string {
597+
return s.replace(/(CO2|CF4|CH4|H2O|N2O|NF3|O2|O3|SF6)/g, (_m, m1) => {
598+
return subscriptMap.get(m1)
589599
})
590600
}

packages/docs-builder/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"esModuleInterop": true,
99
// Enable strict enforcement of `import type`
1010
"verbatimModuleSyntax": true,
11+
"skipLibCheck": true,
1112
"noImplicitAny": false,
1213
"noUnusedLocals": false,
1314
"noUnusedParameters": false,

0 commit comments

Comments
 (0)