Skip to content

Commit 10a502c

Browse files
committed
Fix indent of attributes, expressions, children
1 parent 3c8822e commit 10a502c

File tree

3 files changed

+179
-45
lines changed

3 files changed

+179
-45
lines changed

lib/index.js

+130-34
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
* @typedef {import('mdast-util-from-markdown').OnEnterError} OnEnterError
99
* @typedef {import('mdast-util-from-markdown').OnExitError} OnExitError
1010
*
11-
* @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
1211
* @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle
13-
* @typedef {import('mdast-util-to-markdown').Map} ToMarkdownMap
12+
* @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
13+
* @typedef {import('mdast-util-to-markdown').State} State
14+
* @typedef {import('mdast-util-to-markdown').Tracker} Tracker
1415
*
1516
* @typedef {import('../index.js').MdxJsxAttributeValueExpression} MdxJsxAttributeValueExpression
1617
* @typedef {import('../index.js').MdxJsxAttribute} MdxJsxAttribute
@@ -61,13 +62,14 @@ import {parseEntities} from 'parse-entities'
6162
import {stringifyPosition} from 'unist-util-stringify-position'
6263
import {VFileMessage} from 'vfile-message'
6364
import {stringifyEntitiesLight} from 'stringify-entities'
64-
import {containerFlow} from 'mdast-util-to-markdown/lib/util/container-flow.js'
6565
import {containerPhrasing} from 'mdast-util-to-markdown/lib/util/container-phrasing.js'
6666
import {indentLines} from 'mdast-util-to-markdown/lib/util/indent-lines.js'
6767
import {track} from 'mdast-util-to-markdown/lib/util/track.js'
6868

6969
// To do: next major: use `state`, use utilities from state, rename `safeOptions` to `info`.
7070

71+
const indent = ' '
72+
7173
/**
7274
* Create an extension for `mdast-util-from-markdown` to enable MDX JSX.
7375
*
@@ -521,21 +523,28 @@ export function mdxJsxToMarkdown(options) {
521523
*/
522524
// eslint-disable-next-line complexity
523525
function mdxElement(node, _, context, safeOptions) {
524-
const tracker = track(safeOptions)
525-
const selfClosing =
526-
node.name && (!node.children || node.children.length === 0)
527-
const exit = context.enter(node.type)
528-
let index = -1
526+
const selfClosing = node.name
527+
? !node.children || node.children.length === 0
528+
: false
529+
const depth = inferDepth(context)
530+
const currentIndent = createIndent(depth)
531+
const trackerOneLine = track(safeOptions)
532+
const trackerMultiLine = track(safeOptions)
529533
/** @type {Array<string>} */
530534
const serializedAttributes = []
531-
let value = tracker.move('<' + (node.name || ''))
535+
const prefix = currentIndent + '<' + (node.name || '')
536+
const exit = context.enter(node.type)
537+
538+
trackerOneLine.move(prefix)
539+
trackerMultiLine.move(prefix)
532540

533541
// None.
534542
if (node.attributes && node.attributes.length > 0) {
535543
if (!node.name) {
536544
throw new Error('Cannot serialize fragment w/ attributes')
537545
}
538546

547+
let index = -1
539548
while (++index < node.attributes.length) {
540549
const attribute = node.attributes[index]
541550
/** @type {string} */
@@ -585,7 +594,7 @@ export function mdxJsxToMarkdown(options) {
585594
// Including a line ending (expressions).
586595
(/\r?\n|\r/.test(attributesOnOneLine) ||
587596
// Current position (including `<tag`).
588-
tracker.current().now.column +
597+
trackerOneLine.current().now.column +
589598
// -1 because columns, +1 for ` ` before attributes.
590599
// Attributes joined by spaces.
591600
attributesOnOneLine.length +
@@ -596,18 +605,28 @@ export function mdxJsxToMarkdown(options) {
596605
attributesOnTheirOwnLine = true
597606
}
598607

608+
let tracker = trackerOneLine
609+
let value = prefix
610+
599611
if (attributesOnTheirOwnLine) {
612+
tracker = trackerMultiLine
613+
614+
let index = -1
615+
616+
while (++index < serializedAttributes.length) {
617+
// Only indent first line of of attributes, we can’t indent attribute
618+
// values.
619+
serializedAttributes[index] =
620+
currentIndent + indent + serializedAttributes[index]
621+
}
622+
600623
value += tracker.move(
601-
'\n' + indentLines(serializedAttributes.join('\n'), map)
624+
'\n' + serializedAttributes.join('\n') + '\n' + currentIndent
602625
)
603626
} else if (attributesOnOneLine) {
604627
value += tracker.move(' ' + attributesOnOneLine)
605628
}
606629

607-
if (attributesOnTheirOwnLine) {
608-
value += tracker.move('\n')
609-
}
610-
611630
if (selfClosing) {
612631
value += tracker.move(
613632
(tightSelfClosing || attributesOnTheirOwnLine ? '' : ' ') + '/'
@@ -617,41 +636,118 @@ export function mdxJsxToMarkdown(options) {
617636
value += tracker.move('>')
618637

619638
if (node.children && node.children.length > 0) {
620-
if (node.type === 'mdxJsxFlowElement') {
621-
tracker.shift(2)
622-
value += tracker.move('\n')
623-
value += tracker.move(
624-
indentLines(containerFlow(node, context, tracker.current()), map)
625-
)
626-
value += tracker.move('\n')
627-
} else {
639+
if (node.type === 'mdxJsxTextElement') {
628640
value += tracker.move(
629641
containerPhrasing(node, context, {
630642
...tracker.current(),
631-
before: '<',
632-
after: '>'
643+
before: '>',
644+
after: '<'
633645
})
634646
)
647+
} else {
648+
tracker.shift(2)
649+
value += tracker.move('\n')
650+
value += tracker.move(containerFlow(node, context, tracker.current()))
651+
value += tracker.move('\n')
635652
}
636653
}
637654

638655
if (!selfClosing) {
639-
value += tracker.move('</' + (node.name || '') + '>')
656+
value += tracker.move(currentIndent + '</' + (node.name || '') + '>')
640657
}
641658

642659
exit()
643660
return value
644661
}
662+
}
663+
664+
// Modified copy of:
665+
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/a381cbc/lib/util/container-flow.js>.
666+
//
667+
// To do: add `indent` support to `mdast-util-to-markdown`.
668+
// As indents are only used for JSX, it’s fine for now, but perhaps better
669+
// there.
670+
/**
671+
* @param {MdxJsxFlowElement} parent
672+
* Parent of flow nodes.
673+
* @param {State} state
674+
* Info passed around about the current state.
675+
* @param {ReturnType<Tracker['current']>} info
676+
* Info on where we are in the document we are generating.
677+
* @returns {string}
678+
* Serialized children, joined by (blank) lines.
679+
*/
680+
function containerFlow(parent, state, info) {
681+
const indexStack = state.indexStack
682+
const children = parent.children
683+
const tracker = state.createTracker(info)
684+
const currentIndent = createIndent(inferDepth(state))
685+
/** @type {Array<string>} */
686+
const results = []
687+
let index = -1
688+
689+
indexStack.push(-1)
690+
691+
while (++index < children.length) {
692+
const child = children[index]
645693

646-
/** @type {ToMarkdownMap} */
647-
function map(line, _, blank) {
648-
return (blank ? '' : ' ') + line
694+
indexStack[indexStack.length - 1] = index
695+
696+
const childInfo = {before: '\n', after: '\n', ...tracker.current()}
697+
698+
const result = state.handle(child, parent, state, childInfo)
699+
700+
const serializedChild =
701+
child.type === 'mdxJsxFlowElement'
702+
? result
703+
: indentLines(result, function (line, _, blank) {
704+
return (blank ? '' : currentIndent) + line
705+
})
706+
707+
results.push(tracker.move(serializedChild))
708+
709+
if (child.type !== 'list') {
710+
state.bulletLastUsed = undefined
711+
}
712+
713+
if (index < children.length - 1) {
714+
results.push(tracker.move('\n\n'))
715+
}
649716
}
650717

651-
/**
652-
* @type {ToMarkdownHandle}
653-
*/
654-
function peekElement() {
655-
return '<'
718+
indexStack.pop()
719+
720+
return results.join('')
721+
}
722+
723+
/**
724+
*
725+
* @param {State} context
726+
* @returns {number}
727+
*/
728+
function inferDepth(context) {
729+
let depth = 0
730+
731+
for (const x of context.stack) {
732+
if (x === 'mdxJsxFlowElement') {
733+
depth++
734+
}
656735
}
736+
737+
return depth
738+
}
739+
740+
/**
741+
* @param {number} depth
742+
* @returns {string}
743+
*/
744+
function createIndent(depth) {
745+
return indent.repeat(depth)
746+
}
747+
748+
/**
749+
* @type {ToMarkdownHandle}
750+
*/
751+
function peekElement() {
752+
return '<'
657753
}

readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ import {mdxJsxFromMarkdown, mdxJsxToMarkdown} from 'mdast-util-mdx-jsx'
118118
const doc = await fs.readFile('example.mdx')
119119

120120
const tree = fromMarkdown(doc, {
121-
extensions: [mdxJsx({acorn: acorn, addResult: true})],
121+
extensions: [mdxJsx({acorn, addResult: true})],
122122
mdastExtensions: [mdxJsxFromMarkdown()]
123123
})
124124

test.js

+48-10
Original file line numberDiff line numberDiff line change
@@ -1987,58 +1987,96 @@ test('mdxJsxToMarkdown', () => {
19871987
},
19881988
{extensions: [mdxJsxToMarkdown({printWidth: 20})]}
19891989
),
1990-
'<x\n {\n ...a\n }\n/>\n',
1990+
'<x\n {\n ...a\n}\n/>\n',
19911991
'should support attributes on separate lines if they contain line endings'
19921992
)
19931993
})
19941994

19951995
test('roundtrip', () => {
1996-
equal('<a x="a\nb\nc" />', '<a\n x="a\n b\n c"\n/>\n', 'attribute')
1996+
equal('<a x="a\nb\nc" />', '<a\n x="a\nb\nc"\n/>\n', 'attribute')
19971997

19981998
equal(
19991999
'<a>\n<b x="a\nb\nc" />\n</a>',
2000-
'<a>\n <b\n x="a\n b\n c"\n />\n</a>\n',
2000+
'<a>\n <b\n x="a\nb\nc"\n />\n</a>\n',
20012001
'attribute in nested element'
20022002
)
20032003

20042004
equal(
20052005
'<a>\n <b>\n <c x="a\nb\nc" />\n </b>\n</a>',
2006-
'<a>\n <b>\n <c\n x="a\n b\n c"\n />\n </b>\n</a>\n',
2006+
'<a>\n <b>\n <c\n x="a\nb\nc"\n />\n </b>\n</a>\n',
20072007
'attribute in nested elements'
20082008
)
20092009

20102010
equal(
20112011
'<a x={`a\nb\nc`} />',
2012-
'<a\n x={`a\n b\n c`}\n/>\n',
2012+
'<a\n x={`a\nb\nc`}\n/>\n',
20132013
'attribute expression'
20142014
)
20152015

20162016
equal(
20172017
'<a>\n<b x={`a\nb\nc`} />\n</a>',
2018-
'<a>\n <b\n x={`a\n b\n c`}\n />\n</a>\n',
2018+
'<a>\n <b\n x={`a\nb\nc`}\n />\n</a>\n',
20192019
'attribute expression in nested element'
20202020
)
20212021

20222022
equal(
20232023
'<a>\n <b>\n <c x={`a\nb\nc`} />\n </b>\n</a>',
2024-
'<a>\n <b>\n <c\n x={`a\n b\n c`}\n />\n </b>\n</a>\n',
2024+
'<a>\n <b>\n <c\n x={`a\nb\nc`}\n />\n </b>\n</a>\n',
20252025
'attribute expression in nested elements'
20262026
)
20272027

2028-
equal('<a {\n...a\n} />', '<a\n {\n ...a\n }\n/>\n', 'expression')
2028+
equal('<a {\n...a\n} />', '<a\n {\n...a\n}\n/>\n', 'expression')
20292029

20302030
equal(
20312031
'<a>\n<b {\n...a\n} />\n</a>',
2032-
'<a>\n <b\n {\n ...a\n }\n />\n</a>\n',
2032+
'<a>\n <b\n {\n...a\n}\n />\n</a>\n',
20332033
'expression in nested element'
20342034
)
20352035

20362036
equal(
20372037
'<a>\n <b>\n <c {\n...a\n} />\n </b>\n</a>',
2038-
'<a>\n <b>\n <c\n {\n ...a\n }\n />\n </b>\n</a>\n',
2038+
'<a>\n <b>\n <c\n {\n...a\n}\n />\n </b>\n</a>\n',
20392039
'expression in nested elements'
20402040
)
20412041

2042+
equal(
2043+
`<a>
2044+
<b>
2045+
<c>
2046+
> # d
2047+
- e
2048+
---
2049+
1. f
2050+
~~~js
2051+
g
2052+
~~~
2053+
<h/>
2054+
</c>
2055+
</b>
2056+
</a>`,
2057+
`<a>
2058+
<b>
2059+
<c>
2060+
> # d
2061+
2062+
* e
2063+
2064+
***
2065+
2066+
1. f
2067+
2068+
\`\`\`js
2069+
g
2070+
\`\`\`
2071+
2072+
<h />
2073+
</c>
2074+
</b>
2075+
</a>
2076+
`,
2077+
'children in nested elements'
2078+
)
2079+
20422080
/**
20432081
* @param {string} input
20442082
* @param {string} output

0 commit comments

Comments
 (0)