retext-smartypants
Advanced tools
Comparing version 5.1.0 to 5.2.0
@@ -19,2 +19,15 @@ /** | ||
/** | ||
* Quote characters. | ||
*/ | ||
export type QuoteCharacterMap = { | ||
/** | ||
* Character to use for double quotes. | ||
*/ | ||
double: string | ||
/** | ||
* Character to use for single quotes. | ||
*/ | ||
single: string | ||
} | ||
/** | ||
* Configuration. | ||
@@ -31,2 +44,10 @@ */ | ||
/** | ||
* Characters to use for opening double and single quotes. | ||
*/ | ||
openingQuotes?: QuoteCharacterMap | undefined | ||
/** | ||
* Characters to use for closing double and single quotes. | ||
*/ | ||
closingQuotes?: QuoteCharacterMap | undefined | ||
/** | ||
* Create smart ellipses. | ||
@@ -33,0 +54,0 @@ * |
385
index.js
@@ -9,2 +9,9 @@ /** | ||
* | ||
* @typedef QuoteCharacterMap | ||
* Quote characters. | ||
* @property {string} double | ||
* Character to use for double quotes. | ||
* @property {string} single | ||
* Character to use for single quotes. | ||
* | ||
* @typedef Options | ||
@@ -17,2 +24,6 @@ * Configuration. | ||
* quotes. | ||
* @property {QuoteCharacterMap} [openingQuotes] | ||
* Characters to use for opening double and single quotes. | ||
* @property {QuoteCharacterMap} [closingQuotes] | ||
* Characters to use for closing double and single quotes. | ||
* @property {boolean} [ellipses=true] | ||
@@ -55,205 +66,219 @@ * Create smart ellipses. | ||
const closingQuotes = {'"': '”', "'": '’'} | ||
const openingQuotes = {'"': '“', "'": '‘'} | ||
const defaultClosingQuotes = {'"': '”', "'": '’'} | ||
const defaultOpeningQuotes = {'"': '“', "'": '‘'} | ||
const educators = { | ||
dashes: { | ||
/** | ||
* Transform two dahes into an em-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node) { | ||
if (node.value === '--') { | ||
node.value = '—' | ||
/** | ||
* @param {Options} options | ||
*/ | ||
function createEducators(options) { | ||
const closingQuotes = options.closingQuotes | ||
? {'"': options.closingQuotes.double, "'": options.closingQuotes.single} | ||
: defaultClosingQuotes | ||
const openingQuotes = options.openingQuotes | ||
? {'"': options.openingQuotes.double, "'": options.openingQuotes.single} | ||
: defaultOpeningQuotes | ||
const educators = { | ||
dashes: { | ||
/** | ||
* Transform two dahes into an em-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node) { | ||
if (node.value === '--') { | ||
node.value = '—' | ||
} | ||
}, | ||
/** | ||
* Transform three dahes into an em-dash, and two into an en-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
oldschool(node) { | ||
if (node.value === '---') { | ||
node.value = '—' | ||
} else if (node.value === '--') { | ||
node.value = '–' | ||
} | ||
}, | ||
/** | ||
* Transform three dahes into an en-dash, and two into an em-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
inverted(node) { | ||
if (node.value === '---') { | ||
node.value = '–' | ||
} else if (node.value === '--') { | ||
node.value = '—' | ||
} | ||
} | ||
}, | ||
/** | ||
* Transform three dahes into an em-dash, and two into an en-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
oldschool(node) { | ||
if (node.value === '---') { | ||
node.value = '—' | ||
} else if (node.value === '--') { | ||
node.value = '–' | ||
backticks: { | ||
/** | ||
* Transform double backticks and single quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node) { | ||
if (node.value === '``') { | ||
node.value = '“' | ||
} else if (node.value === "''") { | ||
node.value = '”' | ||
} | ||
}, | ||
/** | ||
* Transform single and double backticks and single quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
all(node, index, parent) { | ||
educators.backticks.true(node, index, parent) | ||
if (node.value === '`') { | ||
node.value = '‘' | ||
} else if (node.value === "'") { | ||
node.value = '’' | ||
} | ||
} | ||
}, | ||
/** | ||
* Transform three dahes into an en-dash, and two into an em-dash. | ||
* | ||
* @type {Method} | ||
*/ | ||
inverted(node) { | ||
if (node.value === '---') { | ||
node.value = '–' | ||
} else if (node.value === '--') { | ||
node.value = '—' | ||
} | ||
} | ||
}, | ||
backticks: { | ||
/** | ||
* Transform double backticks and single quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node) { | ||
if (node.value === '``') { | ||
node.value = '“' | ||
} else if (node.value === "''") { | ||
node.value = '”' | ||
} | ||
}, | ||
/** | ||
* Transform single and double backticks and single quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
all(node, index, parent) { | ||
educators.backticks.true(node, index, parent) | ||
ellipses: { | ||
/** | ||
* Transform multiple dots into unicode ellipses. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node, index, parent) { | ||
const value = node.value | ||
const siblings = parent.children | ||
if (node.value === '`') { | ||
node.value = '‘' | ||
} else if (node.value === "'") { | ||
node.value = '’' | ||
} | ||
} | ||
}, | ||
ellipses: { | ||
/** | ||
* Transform multiple dots into unicode ellipses. | ||
* | ||
* @type {Method} | ||
*/ | ||
true(node, index, parent) { | ||
const value = node.value | ||
const siblings = parent.children | ||
// Simple node with three dots and without white-space. | ||
if (/^\.{3,}$/.test(node.value)) { | ||
node.value = '…' | ||
return | ||
} | ||
// Simple node with three dots and without white-space. | ||
if (/^\.{3,}$/.test(node.value)) { | ||
node.value = '…' | ||
return | ||
} | ||
if (!/^\.+$/.test(value)) { | ||
return | ||
} | ||
if (!/^\.+$/.test(value)) { | ||
return | ||
} | ||
// Search for dot-nodes with white-space between. | ||
/** @type {Array<SentenceContent>} */ | ||
const nodes = [] | ||
let position = index | ||
let count = 1 | ||
// Search for dot-nodes with white-space between. | ||
/** @type {SentenceContent[]} */ | ||
const nodes = [] | ||
let position = index | ||
let count = 1 | ||
// It’s possible that the node is merged with an adjacent word-node. In that | ||
// code, we cannot transform it because there’s no reference to the | ||
// grandparent. | ||
while (--position > 0) { | ||
let sibling = siblings[position] | ||
// It’s possible that the node is merged with an adjacent word-node. In that | ||
// code, we cannot transform it because there’s no reference to the | ||
// grandparent. | ||
while (--position > 0) { | ||
let sibling = siblings[position] | ||
if (sibling.type !== 'WhiteSpaceNode') { | ||
break | ||
} | ||
if (sibling.type !== 'WhiteSpaceNode') { | ||
break | ||
} | ||
const queue = sibling | ||
sibling = siblings[--position] | ||
const queue = sibling | ||
sibling = siblings[--position] | ||
if ( | ||
sibling && | ||
(sibling.type === 'PunctuationNode' || | ||
sibling.type === 'SymbolNode') && | ||
/^\.+$/.test(sibling.value) | ||
) { | ||
nodes.push(queue, sibling) | ||
if ( | ||
sibling && | ||
(sibling.type === 'PunctuationNode' || | ||
sibling.type === 'SymbolNode') && | ||
/^\.+$/.test(sibling.value) | ||
) { | ||
nodes.push(queue, sibling) | ||
count++ | ||
count++ | ||
continue | ||
} | ||
continue | ||
break | ||
} | ||
break | ||
} | ||
if (count < 3) { | ||
return | ||
} | ||
if (count < 3) { | ||
return | ||
siblings.splice(index - nodes.length, nodes.length) | ||
node.value = '…' | ||
} | ||
}, | ||
quotes: { | ||
/** | ||
* Transform straight single- and double quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
// eslint-disable-next-line complexity | ||
true(node, index, parent) { | ||
const siblings = parent.children | ||
const value = node.value | ||
siblings.splice(index - nodes.length, nodes.length) | ||
if (value !== '"' && value !== "'") { | ||
return | ||
} | ||
node.value = '…' | ||
} | ||
}, | ||
quotes: { | ||
/** | ||
* Transform straight single- and double quotes into smart quotes. | ||
* | ||
* @type {Method} | ||
*/ | ||
// eslint-disable-next-line complexity | ||
true(node, index, parent) { | ||
const siblings = parent.children | ||
const value = node.value | ||
const previous = siblings[index - 1] | ||
const next = siblings[index + 1] | ||
const nextNext = siblings[index + 2] | ||
const nextValue = next && toString(next) | ||
if (value !== '"' && value !== "'") { | ||
return | ||
if ( | ||
next && | ||
nextNext && | ||
(next.type === 'PunctuationNode' || next.type === 'SymbolNode') && | ||
nextNext.type !== 'WordNode' | ||
) { | ||
// Special case if the very first character is a quote followed by | ||
// punctuation at a non-word-break. Close the quotes by brute force. | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
nextNext && | ||
(nextValue === '"' || nextValue === "'") && | ||
nextNext.type === 'WordNode' | ||
) { | ||
// Special case for double sets of quotes: | ||
// `He said, "'Quoted' words in a larger quote."` | ||
node.value = openingQuotes[value] | ||
// @ts-expect-error: it’s a literal. | ||
next.value = openingQuotes[nextValue] | ||
} else if (next && /^\d\ds$/.test(nextValue)) { | ||
// Special case for decade abbreviations: `the '80s` | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
previous && | ||
next && | ||
(previous.type === 'WhiteSpaceNode' || | ||
previous.type === 'PunctuationNode' || | ||
previous.type === 'SymbolNode') && | ||
next.type === 'WordNode' | ||
) { | ||
// Get most opening single quotes. | ||
node.value = openingQuotes[value] | ||
} else if ( | ||
previous && | ||
previous.type !== 'WhiteSpaceNode' && | ||
previous.type !== 'SymbolNode' && | ||
previous.type !== 'PunctuationNode' | ||
) { | ||
// Closing quotes. | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
!next || | ||
next.type === 'WhiteSpaceNode' || | ||
(value === "'" && nextValue === 's') | ||
) { | ||
node.value = closingQuotes[value] | ||
} else { | ||
node.value = openingQuotes[value] | ||
} | ||
} | ||
const previous = siblings[index - 1] | ||
const next = siblings[index + 1] | ||
const nextNext = siblings[index + 2] | ||
const nextValue = next && toString(next) | ||
if ( | ||
next && | ||
nextNext && | ||
(next.type === 'PunctuationNode' || next.type === 'SymbolNode') && | ||
nextNext.type !== 'WordNode' | ||
) { | ||
// Special case if the very first character is a quote followed by | ||
// punctuation at a non-word-break. Close the quotes by brute force. | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
nextNext && | ||
(nextValue === '"' || nextValue === "'") && | ||
nextNext.type === 'WordNode' | ||
) { | ||
// Special case for double sets of quotes: | ||
// `He said, "'Quoted' words in a larger quote."` | ||
node.value = openingQuotes[value] | ||
// @ts-expect-error: it’s a literal. | ||
next.value = openingQuotes[nextValue] | ||
} else if (next && /^\d\ds$/.test(nextValue)) { | ||
// Special case for decade abbreviations: `the '80s` | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
previous && | ||
next && | ||
(previous.type === 'WhiteSpaceNode' || | ||
previous.type === 'PunctuationNode' || | ||
previous.type === 'SymbolNode') && | ||
next.type === 'WordNode' | ||
) { | ||
// Get most opening single quotes. | ||
node.value = openingQuotes[value] | ||
} else if ( | ||
previous && | ||
previous.type !== 'WhiteSpaceNode' && | ||
previous.type !== 'SymbolNode' && | ||
previous.type !== 'PunctuationNode' | ||
) { | ||
// Closing quotes. | ||
node.value = closingQuotes[value] | ||
} else if ( | ||
!next || | ||
next.type === 'WhiteSpaceNode' || | ||
(value === "'" && nextValue === 's') | ||
) { | ||
node.value = closingQuotes[value] | ||
} else { | ||
node.value = openingQuotes[value] | ||
} | ||
} | ||
} | ||
return educators | ||
} | ||
@@ -268,3 +293,3 @@ | ||
export default function retextSmartypants(options = {}) { | ||
/** @type {Array.<Method>} */ | ||
/** @type {Array<Method>} */ | ||
const methods = [] | ||
@@ -360,2 +385,4 @@ /** @type {Options['quotes']} */ | ||
const educators = createEducators(options) | ||
if (quotes !== false) { | ||
@@ -362,0 +389,0 @@ methods.push(educators.quotes.true) |
{ | ||
"name": "retext-smartypants", | ||
"version": "5.1.0", | ||
"version": "5.2.0", | ||
"description": "retext plugin to implement SmartyPants", | ||
@@ -51,3 +51,3 @@ "license": "MIT", | ||
"typescript": "^4.0.0", | ||
"xo": "^0.44.0" | ||
"xo": "^0.50.0" | ||
}, | ||
@@ -54,0 +54,0 @@ "scripts": { |
116
readme.md
@@ -11,11 +11,34 @@ # retext-smartypants | ||
[**retext**][retext] plugin to implement [SmartyPants][]. | ||
**[retext][]** plugin to apply [SmartyPants][]. | ||
## Contents | ||
* [What is this?](#what-is-this) | ||
* [When should I use this?](#when-should-i-use-this) | ||
* [Install](#install) | ||
* [Use](#use) | ||
* [API](#api) | ||
* [`unified().use(retextSmartypants[, options])`](#unifieduseretextsmartypants-options) | ||
* [Types](#types) | ||
* [Compatibility](#compatibility) | ||
* [Contribute](#contribute) | ||
* [License](#license) | ||
## What is this? | ||
This package is a [unified][] ([retext][]) plugin to apply [SmartyPants][] to | ||
the syntax tree. | ||
It replaces straight/typewriter punctuation marks and symbols with smart/curly | ||
marks and symbols. | ||
## When should I use this? | ||
You can use this plugin any time there straight marks and symbols in prose, | ||
but you want to use smart ones instead. | ||
## Install | ||
This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): | ||
Node 12+ is needed to use it and it must be `import`ed instead of `require`d. | ||
This package is [ESM only][esm]. | ||
In Node.js (version 12.20+, 14.14+, 16.0+, or 18.0+), install with [npm][]: | ||
[npm][]: | ||
```sh | ||
@@ -25,2 +48,16 @@ npm install retext-smartypants | ||
In Deno with [`esm.sh`][esmsh]: | ||
```js | ||
import retextSmartypants from 'https://esm.sh/retext-smartypants@5' | ||
``` | ||
In browsers with [`esm.sh`][esmsh]: | ||
```html | ||
<script type="module"> | ||
import retextSmartypants from 'https://esm.sh/retext-smartypants@5?bundle' | ||
</script> | ||
``` | ||
## Use | ||
@@ -32,5 +69,5 @@ | ||
const file = retext() | ||
const file = await retext() | ||
.use(retextSmartypants) | ||
.processSync('He said, "A \'simple\' english sentence. . ."') | ||
.process('He said, "A \'simple\' english sentence. . ."') | ||
@@ -53,7 +90,8 @@ console.log(String(file)) | ||
Replaces dumb/straight/typewriter punctuation marks with smart/curly punctuation | ||
marks. | ||
Apply [SmartyPants][]. | ||
##### `options` | ||
Configuration (optional). | ||
###### `options.quotes` | ||
@@ -64,3 +102,13 @@ | ||
Converts straight double and single quotes to smart double or single quotes. | ||
The options `options.openingQuotes` and `options.closingQuotes` affect which | ||
quotes are considered smart. | ||
###### `options.openingQuotes` | ||
Characters to use for opening quotes `{single: '‘', double: '“'}`. | ||
###### `options.closingQuotes` | ||
Characters to use for closing quotes `{single: '’', double: '”'}`. | ||
###### `options.ellipses` | ||
@@ -70,4 +118,4 @@ | ||
Converts triple dot characters (with or without spaces between) into a single | ||
Unicode ellipsis character | ||
Converts triple dot characters (with or without spaces) into a single unicode | ||
ellipsis character. | ||
@@ -78,10 +126,10 @@ ###### `options.backticks` | ||
When `true`, converts double back-ticks into an opening double quote, and | ||
When `true`, converts double backticks into an opening double quote, and | ||
double straight single quotes into a closing double quote. | ||
When `'all'`: does the preceding and converts single back-ticks into an | ||
opening single quote, and a straight single quote into a closing single | ||
smart quote. | ||
When `'all'`: does the what `true` does with the addition of converting single | ||
backticks into an opening single quote, and a straight single quote into a | ||
closing single smart quote. | ||
> **Note**: Quotes can not be `true` when `backticks` is `'all'`; | ||
> 👉 **Note**: `options.quotes` can not be `true` when `backticks` is `'all'`. | ||
@@ -92,10 +140,22 @@ ###### `options.dashes` | ||
When `true`, converts two dashes into an em-dash character. | ||
When `true`, converts two dashes into an em dash character. | ||
When `'oldschool'`, converts two dashes into an en-dash, and three dashes into | ||
an em-dash. | ||
When `'oldschool'`, converts two dashes into an en dash, and three dashes into | ||
an em dash. | ||
When `'inverted'`, converts two dashes into an em-dash, and three dashes into | ||
an en-dash. | ||
When `'inverted'`, converts two dashes into an em dash, and three dashes into | ||
an en dash. | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports the additional types `Options` and `QuoteCharacterMap`. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with all maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 12.20+, 14.14+, 16.0+, and 18.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
## Contribute | ||
@@ -145,9 +205,15 @@ | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[esmsh]: https://esm.sh | ||
[typescript]: https://www.typescriptlang.org | ||
[health]: https://github.com/retextjs/.github | ||
[contributing]: https://github.com/retextjs/.github/blob/HEAD/contributing.md | ||
[contributing]: https://github.com/retextjs/.github/blob/main/contributing.md | ||
[support]: https://github.com/retextjs/.github/blob/HEAD/support.md | ||
[support]: https://github.com/retextjs/.github/blob/main/support.md | ||
[coc]: https://github.com/retextjs/.github/blob/HEAD/code-of-conduct.md | ||
[coc]: https://github.com/retextjs/.github/blob/main/code-of-conduct.md | ||
@@ -158,4 +224,6 @@ [license]: license | ||
[unified]: https://github.com/unifiedjs/unified | ||
[retext]: https://github.com/retextjs/retext | ||
[smartypants]: https://daringfireball.net/projects/smartypants |
22534
459
220