const jsdom = require('jsdom')
const { version } = require('../../package.json')
function transformHTML (
src,
{ path, template, data, unified, remarkParse, remarkGfm, remarkRehype, rehypeStringify }
) {
const content = unified()
.use(remarkParse)
.use(remarkGfm, {
singleTilde: false,
})
.use(remarkRehype)
.use(rehypeStringify)
.processSync(src)
.toString()
// Inject this data into the template, using a mustache-like
// replacement scheme.
const html = template.replace(/{{\s*([\w.]+)\s*}}/g, (token, key) => {
switch (key) {
case 'content':
return `
${content}
`
case 'url_path':
return encodeURI(path)
case 'toc':
return ''
case 'title':
case 'section':
case 'description':
return data[key]
case 'config.github_repo':
case 'config.github_branch':
case 'config.github_path':
return data[key.replace(/^config\./, '')]
case 'version':
return version
default:
throw new Error(`warning: unknown token '${token}' in ${path}`)
}
})
const dom = new jsdom.JSDOM(html)
const document = dom.window.document
// Rewrite relative URLs in links and image sources to be relative to
// this file; this is for supporting `file://` links. HTML pages need
// suffix appended.
const links = [
{ tag: 'a', attr: 'href', suffix: '.html' },
{ tag: 'img', attr: 'src' },
]
for (const linktype of links) {
for (const tag of document.querySelectorAll(linktype.tag)) {
let url = tag.getAttribute(linktype.attr)
if (url.startsWith('/')) {
const childDepth = path.split('/').length - 1
const prefix = childDepth > 0 ? '../'.repeat(childDepth) : './'
url = url.replace(/^\//, prefix)
if (linktype.suffix) {
url += linktype.suffix
}
tag.setAttribute(linktype.attr, url)
}
}
}
// Give headers a unique id so that they can be linked within the doc
const headerIds = []
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerText = header.textContent
.replace(/[A-Z]/g, x => x.toLowerCase())
.replace(/ /g, '-')
.replace(/[^a-z0-9-]/g, '')
let headerId = headerText
let headerIncrement = 1
while (document.getElementById(headerId) !== null) {
headerId = headerText + ++headerIncrement
}
headerIds.push(headerId)
header.setAttribute('id', headerId)
}
// Walk the dom and build a table of contents
const tocEl = document.getElementById('_table_of_contents')
if (tocEl) {
const toc = generateTableOfContents(document)
if (toc) {
tocEl.appendChild(toc)
}
}
return dom.serialize()
}
function generateTableOfContents (document) {
const headers = walkHeaders(document.getElementById('_content'))
// The nesting depth of headers are not necessarily the header level.
// (eg, h1 > h3 > h5 is a depth of three even though there's an h5.)
const hierarchy = []
for (const header of headers) {
const level = headerLevel(header)
while (hierarchy.length && hierarchy[hierarchy.length - 1].headerLevel > level) {
hierarchy.pop()
}
if (!hierarchy.length || hierarchy[hierarchy.length - 1].headerLevel < level) {
const newList = document.createElement('ul')
newList.headerLevel = level
if (hierarchy.length) {
hierarchy[hierarchy.length - 1].appendChild(newList)
}
hierarchy.push(newList)
}
const element = document.createElement('li')
const link = document.createElement('a')
link.setAttribute('href', `#${header.getAttribute('id')}`)
link.innerHTML = header.innerHTML
element.appendChild(link)
hierarchy[hierarchy.length - 1].appendChild(element)
}
return hierarchy[0]
}
function walkHeaders (element, headers = []) {
for (const child of element.childNodes) {
if (headerLevel(child)) {
headers.push(child)
}
walkHeaders(child, headers)
}
return headers
}
function headerLevel (node) {
const level = node.tagName ? node.tagName.match(/^[Hh]([123456])$/) : null
return level ? level[1] : 0
}
module.exports = transformHTML