Skip to content

Commit 77a185b

Browse files
committed
Add quote option
Related-to: GH-12.
1 parent f952db1 commit 77a185b

File tree

4 files changed

+232
-152
lines changed

4 files changed

+232
-152
lines changed

index.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import type {
88

99
export {directiveFromMarkdown, directiveToMarkdown} from './lib/index.js'
1010

11+
/**
12+
* Configuration.
13+
*/
14+
export interface ToMarkdownOptions {
15+
/**
16+
* Preferred quote to use around attribute values
17+
* (default: the `quote` used by `mdast-util-to-markdown` for titles).
18+
*/
19+
quote?: '"' | "'" | null | undefined
20+
}
21+
1122
/**
1223
* Fields shared by directives.
1324
*/

lib/index.js

+161-147
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @import {Directives, LeafDirective, TextDirective} from 'mdast-util-directive'
2+
* @import {Directives, LeafDirective, TextDirective, ToMarkdownOptions} from 'mdast-util-directive'
33
* @import {
44
* CompileContext,
55
* Extension as FromMarkdownExtension,
@@ -22,9 +22,10 @@ import {visitParents} from 'unist-util-visit-parents'
2222

2323
const own = {}.hasOwnProperty
2424

25-
const shortcut = /^[^\t\n\r "#'.<=>`}]+$/
25+
/** @type {Readonly<ToMarkdownOptions>} */
26+
const emptyOptions = {}
2627

27-
handleDirective.peek = peekDirective
28+
const shortcut = /^[^\t\n\r "#'.<=>`}]+$/
2829

2930
/**
3031
* Create an extension for `mdast-util-from-markdown` to enable directives in
@@ -80,10 +81,16 @@ export function directiveFromMarkdown() {
8081
* Create an extension for `mdast-util-to-markdown` to enable directives in
8182
* markdown.
8283
*
84+
* @param {Readonly<ToMarkdownOptions> | null | undefined} [options]
85+
* Configuration (optional).
8386
* @returns {ToMarkdownExtension}
8487
* Extension for `mdast-util-to-markdown` to enable directives.
8588
*/
86-
export function directiveToMarkdown() {
89+
export function directiveToMarkdown(options) {
90+
const settings = options || emptyOptions
91+
92+
handleDirective.peek = peekDirective
93+
8794
return {
8895
handlers: {
8996
containerDirective: handleDirective,
@@ -108,6 +115,156 @@ export function directiveToMarkdown() {
108115
{atBreak: true, character: ':', after: ':'}
109116
]
110117
}
118+
119+
/**
120+
* @type {ToMarkdownHandle}
121+
* @param {Directives} node
122+
*/
123+
function handleDirective(node, _, state, info) {
124+
const tracker = state.createTracker(info)
125+
const sequence = fence(node)
126+
const exit = state.enter(node.type)
127+
let value = tracker.move(sequence + (node.name || ''))
128+
/** @type {LeafDirective | Paragraph | TextDirective | undefined} */
129+
let label
130+
131+
if (node.type === 'containerDirective') {
132+
const head = (node.children || [])[0]
133+
label = inlineDirectiveLabel(head) ? head : undefined
134+
} else {
135+
label = node
136+
}
137+
138+
if (label && label.children && label.children.length > 0) {
139+
const exit = state.enter('label')
140+
/** @type {ConstructName} */
141+
const labelType = `${node.type}Label`
142+
const subexit = state.enter(labelType)
143+
value += tracker.move('[')
144+
value += tracker.move(
145+
state.containerPhrasing(label, {
146+
...tracker.current(),
147+
before: value,
148+
after: ']'
149+
})
150+
)
151+
value += tracker.move(']')
152+
subexit()
153+
exit()
154+
}
155+
156+
value += tracker.move(attributes(node, state))
157+
158+
if (node.type === 'containerDirective') {
159+
const head = (node.children || [])[0]
160+
let shallow = node
161+
162+
if (inlineDirectiveLabel(head)) {
163+
shallow = Object.assign({}, node, {children: node.children.slice(1)})
164+
}
165+
166+
if (shallow && shallow.children && shallow.children.length > 0) {
167+
value += tracker.move('\n')
168+
value += tracker.move(state.containerFlow(shallow, tracker.current()))
169+
}
170+
171+
value += tracker.move('\n' + sequence)
172+
}
173+
174+
exit()
175+
return value
176+
}
177+
178+
/**
179+
* @param {Directives} node
180+
* @param {State} state
181+
* @returns {string}
182+
*/
183+
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']
190+
const attributes = node.attributes || {}
191+
/** @type {Array<string>} */
192+
const values = []
193+
/** @type {string | undefined} */
194+
let classesFull
195+
/** @type {string | undefined} */
196+
let classes
197+
/** @type {string | undefined} */
198+
let id
199+
/** @type {string} */
200+
let key
201+
202+
for (key in attributes) {
203+
if (
204+
own.call(attributes, key) &&
205+
attributes[key] !== undefined &&
206+
attributes[key] !== null
207+
) {
208+
const value = String(attributes[key])
209+
210+
if (key === 'id') {
211+
id = shortcut.test(value) ? '#' + value : quoted('id', value)
212+
} else if (key === 'class') {
213+
const list = value.split(/[\t\n\r ]+/g)
214+
/** @type {Array<string>} */
215+
const classesFullList = []
216+
/** @type {Array<string>} */
217+
const classesList = []
218+
let index = -1
219+
220+
while (++index < list.length) {
221+
;(shortcut.test(list[index]) ? classesList : classesFullList).push(
222+
list[index]
223+
)
224+
}
225+
226+
classesFull =
227+
classesFullList.length > 0
228+
? quoted('class', classesFullList.join(' '))
229+
: ''
230+
classes = classesList.length > 0 ? '.' + classesList.join('.') : ''
231+
} else {
232+
values.push(quoted(key, value))
233+
}
234+
}
235+
}
236+
237+
if (classesFull) {
238+
values.unshift(classesFull)
239+
}
240+
241+
if (classes) {
242+
values.unshift(classes)
243+
}
244+
245+
if (id) {
246+
values.unshift(id)
247+
}
248+
249+
return values.length > 0 ? '{' + values.join(' ') + '}' : ''
250+
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+
}
267+
}
111268
}
112269

113270
/**
@@ -276,154 +433,11 @@ function exit(token) {
276433
this.exit(token)
277434
}
278435

279-
/**
280-
* @type {ToMarkdownHandle}
281-
* @param {Directives} node
282-
*/
283-
function handleDirective(node, _, state, info) {
284-
const tracker = state.createTracker(info)
285-
const sequence = fence(node)
286-
const exit = state.enter(node.type)
287-
let value = tracker.move(sequence + (node.name || ''))
288-
/** @type {LeafDirective | Paragraph | TextDirective | undefined} */
289-
let label
290-
291-
if (node.type === 'containerDirective') {
292-
const head = (node.children || [])[0]
293-
label = inlineDirectiveLabel(head) ? head : undefined
294-
} else {
295-
label = node
296-
}
297-
298-
if (label && label.children && label.children.length > 0) {
299-
const exit = state.enter('label')
300-
/** @type {ConstructName} */
301-
const labelType = `${node.type}Label`
302-
const subexit = state.enter(labelType)
303-
value += tracker.move('[')
304-
value += tracker.move(
305-
state.containerPhrasing(label, {
306-
...tracker.current(),
307-
before: value,
308-
after: ']'
309-
})
310-
)
311-
value += tracker.move(']')
312-
subexit()
313-
exit()
314-
}
315-
316-
value += tracker.move(attributes(node, state))
317-
318-
if (node.type === 'containerDirective') {
319-
const head = (node.children || [])[0]
320-
let shallow = node
321-
322-
if (inlineDirectiveLabel(head)) {
323-
shallow = Object.assign({}, node, {children: node.children.slice(1)})
324-
}
325-
326-
if (shallow && shallow.children && shallow.children.length > 0) {
327-
value += tracker.move('\n')
328-
value += tracker.move(state.containerFlow(shallow, tracker.current()))
329-
}
330-
331-
value += tracker.move('\n' + sequence)
332-
}
333-
334-
exit()
335-
return value
336-
}
337-
338436
/** @type {ToMarkdownHandle} */
339437
function peekDirective() {
340438
return ':'
341439
}
342440

343-
/**
344-
* @param {Directives} node
345-
* @param {State} state
346-
* @returns {string}
347-
*/
348-
function attributes(node, state) {
349-
const quote = state.options.quote || '"'
350-
const subset = node.type === 'textDirective' ? [quote] : [quote, '\n', '\r']
351-
const attributes = node.attributes || {}
352-
/** @type {Array<string>} */
353-
const values = []
354-
/** @type {string | undefined} */
355-
let classesFull
356-
/** @type {string | undefined} */
357-
let classes
358-
/** @type {string | undefined} */
359-
let id
360-
/** @type {string} */
361-
let key
362-
363-
for (key in attributes) {
364-
if (
365-
own.call(attributes, key) &&
366-
attributes[key] !== undefined &&
367-
attributes[key] !== null
368-
) {
369-
const value = String(attributes[key])
370-
371-
if (key === 'id') {
372-
id = shortcut.test(value) ? '#' + value : quoted('id', value)
373-
} else if (key === 'class') {
374-
const list = value.split(/[\t\n\r ]+/g)
375-
/** @type {Array<string>} */
376-
const classesFullList = []
377-
/** @type {Array<string>} */
378-
const classesList = []
379-
let index = -1
380-
381-
while (++index < list.length) {
382-
;(shortcut.test(list[index]) ? classesList : classesFullList).push(
383-
list[index]
384-
)
385-
}
386-
387-
classesFull =
388-
classesFullList.length > 0
389-
? quoted('class', classesFullList.join(' '))
390-
: ''
391-
classes = classesList.length > 0 ? '.' + classesList.join('.') : ''
392-
} else {
393-
values.push(quoted(key, value))
394-
}
395-
}
396-
}
397-
398-
if (classesFull) {
399-
values.unshift(classesFull)
400-
}
401-
402-
if (classes) {
403-
values.unshift(classes)
404-
}
405-
406-
if (id) {
407-
values.unshift(id)
408-
}
409-
410-
return values.length > 0 ? '{' + values.join(' ') + '}' : ''
411-
412-
/**
413-
* @param {string} key
414-
* @param {string} value
415-
* @returns {string}
416-
*/
417-
function quoted(key, value) {
418-
return (
419-
key +
420-
(value
421-
? '=' + quote + stringifyEntitiesLight(value, {subset}) + quote
422-
: '')
423-
)
424-
}
425-
}
426-
427441
/**
428442
* @param {Nodes} node
429443
* @returns {node is Paragraph & {data: {directiveLabel: true}}}

0 commit comments

Comments
 (0)