Skip to content

Commit 7c8d606

Browse files
committed
Add quoteSmart option
Related-to: GH-12.
1 parent 77a185b commit 7c8d606

File tree

5 files changed

+217
-28
lines changed

5 files changed

+217
-28
lines changed

index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export {directiveFromMarkdown, directiveToMarkdown} from './lib/index.js'
1212
* Configuration.
1313
*/
1414
export interface ToMarkdownOptions {
15+
/**
16+
* Use the other quote if that results in less bytes
17+
* (default: `false`).
18+
*/
19+
quoteSmart?: boolean | null | undefined
1520
/**
1621
* Preferred quote to use around attribute values
1722
* (default: the `quote` used by `mdast-util-to-markdown` for titles).

lib/index.js

+48-25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* @import {Nodes, Paragraph} from 'mdast'
1616
*/
1717

18+
import {ccount} from 'ccount'
1819
import {ok as assert} from 'devlop'
1920
import {parseEntities} from 'parse-entities'
2021
import {stringifyEntitiesLight} from 'stringify-entities'
@@ -88,6 +89,16 @@ export function directiveFromMarkdown() {
8889
*/
8990
export function directiveToMarkdown(options) {
9091
const settings = options || emptyOptions
92+
if (
93+
settings.quote !== '"' &&
94+
settings.quote !== "'" &&
95+
settings.quote !== null &&
96+
settings.quote !== undefined
97+
) {
98+
throw new Error(
99+
'Invalid quote `' + settings.quote + '`, expected `\'` or `"`'
100+
)
101+
}
91102

92103
handleDirective.peek = peekDirective
93104

@@ -181,12 +192,6 @@ export function directiveToMarkdown(options) {
181192
* @returns {string}
182193
*/
183194
function attributes(node, state) {
184-
// If the alternative is less common than `quote`, switch.
185-
const appliedQuote = settings.quote || state.options.quote || '"'
186-
const subset =
187-
node.type === 'textDirective'
188-
? [appliedQuote]
189-
: [appliedQuote, '\n', '\r']
190195
const attributes = node.attributes || {}
191196
/** @type {Array<string>} */
192197
const values = []
@@ -208,7 +213,9 @@ export function directiveToMarkdown(options) {
208213
const value = String(attributes[key])
209214

210215
if (key === 'id') {
211-
id = shortcut.test(value) ? '#' + value : quoted('id', value)
216+
id = shortcut.test(value)
217+
? '#' + value
218+
: quoted('id', value, node, state)
212219
} else if (key === 'class') {
213220
const list = value.split(/[\t\n\r ]+/g)
214221
/** @type {Array<string>} */
@@ -225,11 +232,11 @@ export function directiveToMarkdown(options) {
225232

226233
classesFull =
227234
classesFullList.length > 0
228-
? quoted('class', classesFullList.join(' '))
235+
? quoted('class', classesFullList.join(' '), node, state)
229236
: ''
230237
classes = classesList.length > 0 ? '.' + classesList.join('.') : ''
231238
} else {
232-
values.push(quoted(key, value))
239+
values.push(quoted(key, value, node, state))
233240
}
234241
}
235242
}
@@ -247,23 +254,39 @@ export function directiveToMarkdown(options) {
247254
}
248255

249256
return values.length > 0 ? '{' + values.join(' ') + '}' : ''
257+
}
250258

251-
/**
252-
* @param {string} key
253-
* @param {string} value
254-
* @returns {string}
255-
*/
256-
function quoted(key, value) {
257-
return (
258-
key +
259-
(value
260-
? '=' +
261-
appliedQuote +
262-
stringifyEntitiesLight(value, {subset}) +
263-
appliedQuote
264-
: '')
265-
)
266-
}
259+
/**
260+
* @param {string} key
261+
* @param {string} value
262+
* @param {Directives} node
263+
* @param {State} state
264+
* @returns {string}
265+
*/
266+
function quoted(key, value, node, state) {
267+
if (!value) return key
268+
269+
// If the alternative is less common than `quote`, switch.
270+
const preferred = settings.quote || state.options.quote || '"'
271+
const alternative = preferred === '"' ? "'" : '"'
272+
// If the alternative is less common than `quote`, switch.
273+
const appliedQuote =
274+
settings.quoteSmart &&
275+
ccount(value, preferred) > ccount(value, alternative)
276+
? alternative
277+
: preferred
278+
const subset =
279+
node.type === 'textDirective'
280+
? [appliedQuote]
281+
: [appliedQuote, '\n', '\r']
282+
283+
return (
284+
key +
285+
('=' +
286+
appliedQuote +
287+
stringifyEntitiesLight(value, {subset}) +
288+
appliedQuote)
289+
)
267290
}
268291
}
269292

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dependencies": {
88
"@types/mdast": "^4.0.0",
99
"@types/unist": "^3.0.0",
10+
"ccount": "^2.0.0",
1011
"devlop": "^1.0.0",
1112
"mdast-util-from-markdown": "^2.0.0",
1213
"mdast-util-to-markdown": "^2.0.0",

readme.md

+3
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ Configuration.
264264

265265
###### Parameters
266266

267+
* `quoteSmart`
268+
(`boolean`, default: `false`)
269+
— use the other quote if that results in less bytes
267270
* `quote`
268271
(`'"'` or `"'"`,
269272
default: the [`quote`][quote] used by `mdast-util-to-markdown` for titles)

test.js

+160-3
Original file line numberDiff line numberDiff line change
@@ -1132,12 +1132,169 @@ test('directiveToMarkdown()', async function (t) {
11321132
attributes: {title: 'a'},
11331133
children: []
11341134
},
1135-
{
1136-
extensions: [directiveToMarkdown({quote: "'"})]
1137-
}
1135+
{extensions: [directiveToMarkdown({quote: "'"})]}
11381136
),
11391137
":i{title='a'}\n"
11401138
)
11411139
}
11421140
)
1141+
1142+
await t.test(
1143+
"should quote attribute values with double quotes if `quote: '\\\"'`",
1144+
async function () {
1145+
assert.deepEqual(
1146+
toMarkdown(
1147+
{
1148+
type: 'textDirective',
1149+
name: 'i',
1150+
attributes: {title: 'a'},
1151+
children: []
1152+
},
1153+
{extensions: [directiveToMarkdown({quote: '"'})]}
1154+
),
1155+
':i{title="a"}\n'
1156+
)
1157+
}
1158+
)
1159+
1160+
await t.test(
1161+
"should quote attribute values with single quotes if `quote: '\\''` even if they occur in value",
1162+
async function () {
1163+
assert.deepEqual(
1164+
toMarkdown(
1165+
{
1166+
type: 'textDirective',
1167+
name: 'i',
1168+
attributes: {title: "'a'"},
1169+
children: []
1170+
},
1171+
{extensions: [directiveToMarkdown({quote: "'"})]}
1172+
),
1173+
":i{title='&#x27;a&#x27;'}\n"
1174+
)
1175+
}
1176+
)
1177+
1178+
await t.test(
1179+
"should quote attribute values with double quotes if `quote: '\\\"'` even if they occur in value",
1180+
async function () {
1181+
assert.deepEqual(
1182+
toMarkdown(
1183+
{
1184+
type: 'textDirective',
1185+
name: 'i',
1186+
attributes: {title: '"a"'},
1187+
children: []
1188+
},
1189+
{extensions: [directiveToMarkdown({quote: '"'})]}
1190+
),
1191+
':i{title="&#x22;a&#x22;"}\n'
1192+
)
1193+
}
1194+
)
1195+
1196+
await t.test('should throw on invalid quotes', async function () {
1197+
assert.throws(function () {
1198+
toMarkdown(
1199+
{
1200+
type: 'textDirective',
1201+
name: 'i',
1202+
attributes: {},
1203+
children: []
1204+
},
1205+
// @ts-expect-error: check how the runtime handles an incorrect `quote`
1206+
{extensions: [directiveToMarkdown({quote: '`'})]}
1207+
)
1208+
}, /Invalid quote ```, expected `'` or `"`/)
1209+
})
1210+
1211+
await t.test(
1212+
'should quote attribute values with primary quotes if they occur less than the alternative',
1213+
async function () {
1214+
assert.deepEqual(
1215+
toMarkdown(
1216+
{
1217+
type: 'textDirective',
1218+
name: 'i',
1219+
attributes: {title: "'\"a'"},
1220+
children: []
1221+
},
1222+
{extensions: [directiveToMarkdown({quoteSmart: true})]}
1223+
),
1224+
':i{title="\'&#x22;a\'"}\n'
1225+
)
1226+
}
1227+
)
1228+
1229+
await t.test(
1230+
'should quote attribute values with primary quotes if they occur as much as alternatives (#1)',
1231+
async function () {
1232+
assert.deepEqual(
1233+
toMarkdown(
1234+
{
1235+
type: 'textDirective',
1236+
name: 'i',
1237+
attributes: {title: '"a\''},
1238+
children: []
1239+
},
1240+
{extensions: [directiveToMarkdown({quoteSmart: true})]}
1241+
),
1242+
':i{title="&#x22;a\'"}\n'
1243+
)
1244+
}
1245+
)
1246+
1247+
await t.test(
1248+
'should quote attribute values with primary quotes if they occur as much as alternatives (#2)',
1249+
async function () {
1250+
assert.deepEqual(
1251+
toMarkdown(
1252+
{
1253+
type: 'textDirective',
1254+
name: 'i',
1255+
attributes: {title: '"\'a\'"'},
1256+
children: []
1257+
},
1258+
{extensions: [directiveToMarkdown({quoteSmart: true})]}
1259+
),
1260+
':i{title="&#x22;\'a\'&#x22;"}\n'
1261+
)
1262+
}
1263+
)
1264+
1265+
await t.test(
1266+
'should quote attribute values with alternative quotes if the primary occurs',
1267+
async function () {
1268+
assert.deepEqual(
1269+
toMarkdown(
1270+
{
1271+
type: 'textDirective',
1272+
name: 'i',
1273+
attributes: {title: '"a"'},
1274+
children: []
1275+
},
1276+
{extensions: [directiveToMarkdown({quoteSmart: true})]}
1277+
),
1278+
':i{title=\'"a"\'}\n'
1279+
)
1280+
}
1281+
)
1282+
1283+
await t.test(
1284+
'should quote attribute values with alternative quotes if they occur less than the primary',
1285+
async function () {
1286+
assert.deepEqual(
1287+
toMarkdown(
1288+
{
1289+
type: 'textDirective',
1290+
name: 'i',
1291+
attributes: {title: '"\'a"'},
1292+
children: []
1293+
},
1294+
{extensions: [directiveToMarkdown({quoteSmart: true})]}
1295+
),
1296+
':i{title=\'"&#x27;a"\'}\n'
1297+
)
1298+
}
1299+
)
11431300
})

0 commit comments

Comments
 (0)