Skip to content

Commit 7eede29

Browse files
committed
Add options.printWidth for spaces or line endings between attributes
1 parent 71e70ad commit 7eede29

File tree

4 files changed

+149
-46
lines changed

4 files changed

+149
-46
lines changed

lib/index.js

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
* @property {boolean} [tightSelfClosing=false]
2626
* Do not use an extra space when closing self-closing elements: `<img/>`
2727
* instead of `<img />`.
28+
* @property {number} [printWidth=Infinity]
29+
* Specify the line length that the printer will wrap on.
30+
* This is not a hard maximum width: things will be printed longer and
31+
* shorter.
32+
*
33+
* Note: this option is only used for JSX tags currently, and might be moved
34+
* to `mdast-util-to-markdown` in the future.
2835
*/
2936

3037
import {ccount} from 'ccount'
@@ -35,6 +42,7 @@ import {stringifyEntitiesLight} from 'stringify-entities'
3542
import {containerFlow} from 'mdast-util-to-markdown/lib/util/container-flow.js'
3643
import {containerPhrasing} from 'mdast-util-to-markdown/lib/util/container-phrasing.js'
3744
import {indentLines} from 'mdast-util-to-markdown/lib/util/indent-lines.js'
45+
import {track} from 'mdast-util-to-markdown/lib/util/track.js'
3846

3947
/** @return {FromMarkdownExtension} */
4048
export function mdxJsxFromMarkdown() {
@@ -366,7 +374,12 @@ export function mdxJsxFromMarkdown() {
366374
* @returns {ToMarkdownExtension}
367375
*/
368376
export function mdxJsxToMarkdown(options = {}) {
369-
const {quote = '"', quoteSmart, tightSelfClosing} = options
377+
const {
378+
quote = '"',
379+
quoteSmart,
380+
tightSelfClosing,
381+
printWidth = Number.POSITIVE_INFINITY
382+
} = options
370383
const alternative = quote === '"' ? "'" : '"'
371384

372385
if (quote !== '"' && quote !== "'") {
@@ -397,28 +410,26 @@ export function mdxJsxToMarkdown(options = {}) {
397410
* @param {MdxJsxFlowElement|MdxJsxTextElement} node
398411
*/
399412
// eslint-disable-next-line complexity
400-
function mdxElement(node, _, context) {
413+
function mdxElement(node, _, context, safeOptions) {
414+
const tracker = track(safeOptions)
401415
const selfClosing =
402416
node.name && (!node.children || node.children.length === 0)
403417
const exit = context.enter(node.type)
404-
let attributeValue = ''
405418
let index = -1
406419
/** @type {Array<string>} */
407-
const attributes = []
408-
/** @type {string} */
409-
let result
420+
const serializedAttributes = []
421+
let value = tracker.move('<' + (node.name || ''))
410422

411423
// None.
412424
if (node.attributes && node.attributes.length > 0) {
413425
if (!node.name) {
414426
throw new Error('Cannot serialize fragment w/ attributes')
415427
}
416428

417-
const isMultiFlow =
418-
node.type === 'mdxJsxFlowElement' && node.attributes.length > 1
419-
420429
while (++index < node.attributes.length) {
421430
const attribute = node.attributes[index]
431+
/** @type {string} */
432+
let result
422433

423434
if (attribute.type === 'mdxJsxExpressionAttribute') {
424435
result = '{' + (attribute.value || '') + '}'
@@ -428,71 +439,109 @@ export function mdxJsxToMarkdown(options = {}) {
428439
}
429440

430441
const value = attribute.value
431-
let initializer = ''
442+
const left = attribute.name
443+
/** @type {string} */
444+
let right = ''
432445

433446
if (value === undefined || value === null) {
434447
// Empty.
435448
} else if (typeof value === 'object') {
436-
initializer = '={' + (value.value || '') + '}'
449+
right = '{' + (value.value || '') + '}'
437450
} else {
438451
// If the alternative is less common than `quote`, switch.
439452
const appliedQuote =
440453
quoteSmart && ccount(value, quote) > ccount(value, alternative)
441454
? alternative
442455
: quote
443-
444-
initializer +=
445-
'=' +
456+
right =
446457
appliedQuote +
447458
stringifyEntitiesLight(value, {subset: [appliedQuote]}) +
448459
appliedQuote
449460
}
450461

451-
result = attribute.name + initializer
462+
result = left + (right ? '=' : '') + right
452463
}
453464

454-
attributes.push((isMultiFlow ? '\n ' : ' ') + result)
465+
serializedAttributes.push(result)
455466
}
467+
}
456468

457-
attributeValue = attributes.join('') + (isMultiFlow ? '\n' : '')
469+
let attributesOnTheirOwnLine = false
470+
const attributesOnOneLine = serializedAttributes.join(' ')
471+
472+
if (
473+
// Block:
474+
node.type === 'mdxJsxFlowElement' &&
475+
// Including a line ending (expressions).
476+
(/\r?\n|\r/.test(attributesOnOneLine) ||
477+
// Current position (including `<tag`).
478+
tracker.current().now.column +
479+
// -1 because columns, +1 for ` ` before attributes.
480+
// Attributes joined by spaces.
481+
attributesOnOneLine.length +
482+
// ` />`.
483+
(selfClosing ? (tightSelfClosing ? 2 : 3) : 1) >
484+
printWidth)
485+
) {
486+
attributesOnTheirOwnLine = true
458487
}
459488

460-
const value =
461-
'<' +
462-
(node.name || '') +
463-
attributeValue +
464-
(selfClosing
465-
? (tightSelfClosing || /[ \n]$/.test(attributeValue) ? '' : ' ') + '/'
466-
: '') +
467-
'>' +
468-
(node.children && node.children.length > 0
469-
? node.type === 'mdxJsxFlowElement'
470-
? '\n' + indent(containerFlow(node, context)) + '\n'
471-
: containerPhrasing(node, context, {before: '<', after: '>'})
472-
: '') +
473-
(selfClosing ? '' : '</' + (node.name || '') + '>')
489+
if (attributesOnTheirOwnLine) {
490+
value += tracker.move(
491+
'\n' + indentLines(serializedAttributes.join('\n'), map)
492+
)
493+
} else if (attributesOnOneLine) {
494+
value += tracker.move(' ' + attributesOnOneLine)
495+
}
496+
497+
if (attributesOnTheirOwnLine) {
498+
value += tracker.move('\n')
499+
}
500+
501+
if (selfClosing) {
502+
value += tracker.move(
503+
(tightSelfClosing || attributesOnTheirOwnLine ? '' : ' ') + '/'
504+
)
505+
}
506+
507+
value += tracker.move('>')
508+
509+
if (node.children && node.children.length > 0) {
510+
if (node.type === 'mdxJsxFlowElement') {
511+
tracker.shift(2)
512+
value += tracker.move('\n')
513+
value += tracker.move(
514+
indentLines(containerFlow(node, context, tracker.current()), map)
515+
)
516+
value += tracker.move('\n')
517+
} else {
518+
value += tracker.move(
519+
containerPhrasing(node, context, {
520+
...tracker.current(),
521+
before: '<',
522+
after: '>'
523+
})
524+
)
525+
}
526+
}
527+
528+
if (!selfClosing) {
529+
value += tracker.move('</' + (node.name || '') + '>')
530+
}
474531

475532
exit()
476533
return value
477534
}
478535

536+
/** @type {ToMarkdownMap} */
537+
function map(line, _, blank) {
538+
return (blank ? '' : ' ') + line
539+
}
540+
479541
/**
480542
* @type {ToMarkdownHandle}
481543
*/
482544
function peekElement() {
483545
return '<'
484546
}
485-
486-
/**
487-
* @param {string} value
488-
* @returns {string}
489-
*/
490-
function indent(value) {
491-
return indentLines(value, map)
492-
493-
/** @type {ToMarkdownMap} */
494-
function map(line, _, blank) {
495-
return (blank ? '' : ' ') + line
496-
}
497-
}
498547
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@types/estree-jsx": "^0.0.1",
4040
"@types/mdast": "^3.0.0",
4141
"ccount": "^2.0.0",
42-
"mdast-util-to-markdown": "^1.0.0",
42+
"mdast-util-to-markdown": "^1.3.0",
4343
"parse-entities": "^4.0.0",
4444
"stringify-entities": "^4.0.0",
4545
"unist-util-remove-position": "^4.0.0",

readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ Use the other quote if that results in less bytes (`boolean`, default: `false`).
245245
Do not use an extra space when closing self-closing elements: `<img/>` instead
246246
of `<img />` (`boolean`, default: `false`).
247247

248+
###### `options.printWidth`
249+
250+
Try and wrap syntax as this width (`number`, default: `Infinity`).
251+
When set to a finite number (say, `80`), the formatter will print attributes on
252+
separate lines when a tag doesn’t fit on one line.
253+
The normal behavior is to print attributes with spaces between them instead of
254+
line endings.
255+
248256
## Syntax tree
249257

250258
The following interfaces are added to **[mdast][]** by this utility.

test.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1572,7 +1572,7 @@ test('mdast -> markdown', (t) => {
15721572
},
15731573
{extensions: [mdxJsxToMarkdown()]}
15741574
),
1575-
'<x\n {y}\n {z}\n/>\n',
1575+
'<x {y} {z} />\n',
15761576
'should serialize flow jsx w/ `name`, multiple `attributes` w/o `children`'
15771577
)
15781578

@@ -1941,5 +1941,51 @@ test('mdast -> markdown', (t) => {
19411941
'should support `options.tightSelfClosing`: space when `true`'
19421942
)
19431943

1944+
t.deepEqual(
1945+
toMarkdown(
1946+
{
1947+
type: 'mdxJsxFlowElement',
1948+
name: 'x',
1949+
attributes: [
1950+
{type: 'mdxJsxAttribute', name: 'y', value: 'aaa'},
1951+
{type: 'mdxJsxAttribute', name: 'z', value: 'aa'}
1952+
],
1953+
children: []
1954+
},
1955+
{extensions: [mdxJsxToMarkdown({printWidth: 20})]}
1956+
),
1957+
'<x y="aaa" z="aa" />\n',
1958+
'should support attributes on one line up to the given `options.printWidth`'
1959+
)
1960+
t.deepEqual(
1961+
toMarkdown(
1962+
{
1963+
type: 'mdxJsxFlowElement',
1964+
name: 'x',
1965+
attributes: [
1966+
{type: 'mdxJsxAttribute', name: 'y', value: 'aaa'},
1967+
{type: 'mdxJsxAttribute', name: 'z', value: 'aaa'}
1968+
],
1969+
children: []
1970+
},
1971+
{extensions: [mdxJsxToMarkdown({printWidth: 20})]}
1972+
),
1973+
'<x\n y="aaa"\n z="aaa"\n/>\n',
1974+
'should support attributes on separate lines up to the given `options.printWidth`'
1975+
)
1976+
t.deepEqual(
1977+
toMarkdown(
1978+
{
1979+
type: 'mdxJsxFlowElement',
1980+
name: 'x',
1981+
attributes: [{type: 'mdxJsxExpressionAttribute', value: '\n ...a\n'}],
1982+
children: []
1983+
},
1984+
{extensions: [mdxJsxToMarkdown({printWidth: 20})]}
1985+
),
1986+
'<x\n {\n ...a\n }\n/>\n',
1987+
'should support attributes on separate lines if they contain line endings'
1988+
)
1989+
19441990
t.end()
19451991
})

0 commit comments

Comments
 (0)