Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
rehype-typeset plugin.js
4.4 kB · 163 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164/** * @import {Root} from 'hast' */
import { SKIP, visit } from 'unist-util-visit'
/** * @typedef {Object} QuotesOptions *//** * @typedef {Object} PunctuationOptions * @property {'double' | 'triple'} em-dash-replacement Replace hyphen * sequences with em-dash glyph. If `triple` is set, two hyphens will * be replaced with an en-dash. *//** * @typedef {Object} SpacesOptions * @property {'open' | 'closed'} [en-dash-spacing] Should en-dashes be surrounded by spaces. *//** * @typedef {Object} PluginOptions * @property {QuotesOptions | false} [quotes] * @property {PunctuationOptions | false} [punctuation] * @property {SpacesOptions | false} [spaces] */
const HAIR_SPACE = '\u200A'const EN_DASH = '\u2013'const EM_DASH = '\u2014'const ELLIPSIS = '\u2026'
const IGNORED_ELEMENTS = new Set(['title', 'script', 'style', 'pre', 'code'])
/** @type {PluginOptions} */const DEFAULT_OPTIONS = { quotes: {}, punctuation: { 'em-dash-replacement': 'double', }, spaces: { 'en-dash-spacing': 'open', },}
/** * Process HTML content for better web typography. * * @param {PluginOptions} [options] */export default function rehypeTypeset(options = DEFAULT_OPTIONS) { /** * @param {Root} tree * @return {undefined} */ return function (tree) { visit(tree, 'element', (node) => { if (IGNORED_ELEMENTS.has(node.tagName)) return SKIP
for (let child of node.children) { if (child.type === 'text') { if (options.quotes) child.value = replaceQuotes(child.value) if (options.punctuation) child.value = replacePunctuation(child.value) if (options.spaces) child.value = replaceSpaces(child.value, options.spaces) } } }) }}
/** * * @param {string} text */function replaceQuotes(text) { text = text.replaceAll(''', "'") text = text.replaceAll('"', '"')
text = text .replaceAll(/(\W|^)"([^\s!?:;.,‽»])/g, '$1\u201C$2') // Beginning " .replaceAll(/(\u201C[^"]*)"([^"]*$|[^\u201C"]*\u201C)/g, '$1\u201D$2') // Ending " .replaceAll(/([^0-9])"/g, '$1\u201D') // Remaining " at end of word .replaceAll(/(\W|^)'(\S)/g, '$1\u2018$2') // Beginning ' .replaceAll(/([a-z])'([a-z])/gi, '$1\u2019$2') // Conjunction's possession .replaceAll(/((\u2018[^']*)|[a-z])'([^0-9]|$)/gi, '$1\u2019$3') // Ending ' .replaceAll( /(\u2018)([0-9]{2}[^\u2019]*)(\u2018([^0-9]|$)|$|\u2019[a-z])/gi, '\u2019$2$3', ) // Abbrev. years like '93 .replaceAll( /(\B|^)\u2018(?=([^\u2019]*\u2019\b)*([^\u2019\u2018]*\W[\u2019\u2018]\b|[^\u2019\u2018]*$))/gi, '$1\u2019', ) // Backwards apostrophe .replaceAll("'''", '\u2034') // Triple prime .replaceAll(/("|'')/g, '\u2033') // Double prime .replaceAll("'", '\u2032')
// Allow escaped quotes text = text .replaceAll(String.raw`\“`, '"') .replaceAll(String.raw`\”`, '"') .replaceAll(String.raw`\’`, "'") .replaceAll(String.raw`\‘`, "'")
return text}
/** * * @param {string} text * * @todo Add option for "open" or "closed" dashes. */function replacePunctuation(text) { /** * Replace hyphens, encoded en-dashes, and em-dashes — which are * surrounded by digits — with an en-dash glyph. * @see https://en.wikipedia.org/wiki/Dash#En_dash */ text = text.replaceAll(/(\d+\s?)-(\s?\d+)/g, `$1${EN_DASH}$2`) text = text.replaceAll(/(\d+\s?)–(\s?\d+)/g, `$1${EN_DASH}$2`) text = text.replaceAll(/(\d+\s?)—|—(\s?\d+)/g, `$1${EN_DASH}$2`)
/** * @see https://en.wikipedia.org/wiki/Dash#Em_dash */ text = text.replaceAll('--', EM_DASH) // Text = text.replaceAll(' – ', ` ${EM_DASH} `)
/** @see https://en.wikipedia.org/wiki/Ellipsis */ text = text.replaceAll('...', ELLIPSIS)
/** * @see https://en.wikipedia.org/wiki/Non-breaking_space */ let NBSP = ' ' let NBSP_PUNCTUATION_START = /([«¿¡]) /g let NBSP_PUNCTUATION_END = / ([!?:;.,‽»])/g
text = text.replaceAll(NBSP_PUNCTUATION_START, '$1' + NBSP) text = text.replaceAll(NBSP_PUNCTUATION_END, NBSP + '$1')
return text}
/** * * @param {string} text * @param {SpacesOptions} [_options] */function replaceSpaces(text, _options) { text = text.replaceAll( /(\d+)\s?–\s?(\d+)/g, `$1${HAIR_SPACE}${EN_DASH}${HAIR_SPACE}$2`, )
text = text.replaceAll(' — ', `${HAIR_SPACE}${EM_DASH}${HAIR_SPACE}`)
text = text.replaceAll(' × ', `${HAIR_SPACE}×${HAIR_SPACE}`) text = text.replaceAll(' / ', `${HAIR_SPACE}/${HAIR_SPACE}`)
return text}