index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. /**
  2. * @import {CompileContext, Extension as FromMarkdownExtension, Handle as FromMarkdownHandle, OnEnterError, OnExitError, Token} from 'mdast-util-from-markdown'
  3. * @import {Handle as ToMarkdownHandle, Options as ToMarkdownExtension, State, Tracker} from 'mdast-util-to-markdown'
  4. * @import {Point} from 'unist'
  5. * @import {MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement} from '../index.js'
  6. */
  7. /**
  8. * @typedef Tag
  9. * Single tag.
  10. * @property {string | undefined} name
  11. * Name of tag, or `undefined` for fragment.
  12. *
  13. * > 👉 **Note**: `null` is used in the AST for fragments, as it serializes in
  14. * > JSON.
  15. * @property {Array<MdxJsxAttribute | MdxJsxExpressionAttribute>} attributes
  16. * Attributes.
  17. * @property {boolean} close
  18. * Whether the tag is closing (`</x>`).
  19. * @property {boolean} selfClosing
  20. * Whether the tag is self-closing (`<x/>`).
  21. * @property {Token['start']} start
  22. * Start point.
  23. * @property {Token['start']} end
  24. * End point.
  25. *
  26. * @typedef ToMarkdownOptions
  27. * Configuration.
  28. * @property {'"' | "'" | null | undefined} [quote='"']
  29. * Preferred quote to use around attribute values (default: `'"'`).
  30. * @property {boolean | null | undefined} [quoteSmart=false]
  31. * Use the other quote if that results in less bytes (default: `false`).
  32. * @property {boolean | null | undefined} [tightSelfClosing=false]
  33. * Do not use an extra space when closing self-closing elements: `<img/>`
  34. * instead of `<img />` (default: `false`).
  35. * @property {number | null | undefined} [printWidth=Infinity]
  36. * Try and wrap syntax at this width (default: `Infinity`).
  37. *
  38. * When set to a finite number (say, `80`), the formatter will print
  39. * attributes on separate lines when a tag doesn’t fit on one line.
  40. * The normal behavior is to print attributes with spaces between them
  41. * instead of line endings.
  42. */
  43. import {ccount} from 'ccount'
  44. import {ok as assert} from 'devlop'
  45. import {parseEntities} from 'parse-entities'
  46. import {stringifyEntitiesLight} from 'stringify-entities'
  47. import {stringifyPosition} from 'unist-util-stringify-position'
  48. import {VFileMessage} from 'vfile-message'
  49. const indent = ' '
  50. /**
  51. * Create an extension for `mdast-util-from-markdown` to enable MDX JSX.
  52. *
  53. * @returns {FromMarkdownExtension}
  54. * Extension for `mdast-util-from-markdown` to enable MDX JSX.
  55. *
  56. * When using the syntax extension with `addResult`, nodes will have a
  57. * `data.estree` field set to an ESTree `Program` node.
  58. */
  59. export function mdxJsxFromMarkdown() {
  60. return {
  61. canContainEols: ['mdxJsxTextElement'],
  62. enter: {
  63. mdxJsxFlowTag: enterMdxJsxTag,
  64. mdxJsxFlowTagClosingMarker: enterMdxJsxTagClosingMarker,
  65. mdxJsxFlowTagAttribute: enterMdxJsxTagAttribute,
  66. mdxJsxFlowTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
  67. mdxJsxFlowTagAttributeValueLiteral: buffer,
  68. mdxJsxFlowTagAttributeValueExpression: buffer,
  69. mdxJsxFlowTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker,
  70. mdxJsxTextTag: enterMdxJsxTag,
  71. mdxJsxTextTagClosingMarker: enterMdxJsxTagClosingMarker,
  72. mdxJsxTextTagAttribute: enterMdxJsxTagAttribute,
  73. mdxJsxTextTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
  74. mdxJsxTextTagAttributeValueLiteral: buffer,
  75. mdxJsxTextTagAttributeValueExpression: buffer,
  76. mdxJsxTextTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker
  77. },
  78. exit: {
  79. mdxJsxFlowTagClosingMarker: exitMdxJsxTagClosingMarker,
  80. mdxJsxFlowTagNamePrimary: exitMdxJsxTagNamePrimary,
  81. mdxJsxFlowTagNameMember: exitMdxJsxTagNameMember,
  82. mdxJsxFlowTagNameLocal: exitMdxJsxTagNameLocal,
  83. mdxJsxFlowTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
  84. mdxJsxFlowTagExpressionAttributeValue: data,
  85. mdxJsxFlowTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
  86. mdxJsxFlowTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
  87. mdxJsxFlowTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
  88. mdxJsxFlowTagAttributeValueLiteralValue: data,
  89. mdxJsxFlowTagAttributeValueExpression:
  90. exitMdxJsxTagAttributeValueExpression,
  91. mdxJsxFlowTagAttributeValueExpressionValue: data,
  92. mdxJsxFlowTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
  93. mdxJsxFlowTag: exitMdxJsxTag,
  94. mdxJsxTextTagClosingMarker: exitMdxJsxTagClosingMarker,
  95. mdxJsxTextTagNamePrimary: exitMdxJsxTagNamePrimary,
  96. mdxJsxTextTagNameMember: exitMdxJsxTagNameMember,
  97. mdxJsxTextTagNameLocal: exitMdxJsxTagNameLocal,
  98. mdxJsxTextTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
  99. mdxJsxTextTagExpressionAttributeValue: data,
  100. mdxJsxTextTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
  101. mdxJsxTextTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
  102. mdxJsxTextTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
  103. mdxJsxTextTagAttributeValueLiteralValue: data,
  104. mdxJsxTextTagAttributeValueExpression:
  105. exitMdxJsxTagAttributeValueExpression,
  106. mdxJsxTextTagAttributeValueExpressionValue: data,
  107. mdxJsxTextTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
  108. mdxJsxTextTag: exitMdxJsxTag
  109. }
  110. }
  111. /**
  112. * @this {CompileContext}
  113. * @type {FromMarkdownHandle}
  114. */
  115. function buffer() {
  116. this.buffer()
  117. }
  118. /**
  119. * Copy a point-like value.
  120. *
  121. * @param {Point} d
  122. * Point-like value.
  123. * @returns {Point}
  124. * unist point.
  125. */
  126. function point(d) {
  127. return {line: d.line, column: d.column, offset: d.offset}
  128. }
  129. /**
  130. * @this {CompileContext}
  131. * @type {FromMarkdownHandle}
  132. */
  133. function data(token) {
  134. this.config.enter.data.call(this, token)
  135. this.config.exit.data.call(this, token)
  136. }
  137. /**
  138. * @this {CompileContext}
  139. * @type {FromMarkdownHandle}
  140. */
  141. function enterMdxJsxTag(token) {
  142. /** @type {Tag} */
  143. const tag = {
  144. name: undefined,
  145. attributes: [],
  146. close: false,
  147. selfClosing: false,
  148. start: token.start,
  149. end: token.end
  150. }
  151. if (!this.data.mdxJsxTagStack) this.data.mdxJsxTagStack = []
  152. this.data.mdxJsxTag = tag
  153. this.buffer()
  154. }
  155. /**
  156. * @this {CompileContext}
  157. * @type {FromMarkdownHandle}
  158. */
  159. function enterMdxJsxTagClosingMarker(token) {
  160. const stack = this.data.mdxJsxTagStack
  161. assert(stack, 'expected `mdxJsxTagStack`')
  162. if (stack.length === 0) {
  163. throw new VFileMessage(
  164. 'Unexpected closing slash `/` in tag, expected an open tag first',
  165. {start: token.start, end: token.end},
  166. 'mdast-util-mdx-jsx:unexpected-closing-slash'
  167. )
  168. }
  169. }
  170. /**
  171. * @this {CompileContext}
  172. * @type {FromMarkdownHandle}
  173. */
  174. function enterMdxJsxTagAnyAttribute(token) {
  175. const tag = this.data.mdxJsxTag
  176. assert(tag, 'expected `mdxJsxTag`')
  177. if (tag.close) {
  178. throw new VFileMessage(
  179. 'Unexpected attribute in closing tag, expected the end of the tag',
  180. {start: token.start, end: token.end},
  181. 'mdast-util-mdx-jsx:unexpected-attribute'
  182. )
  183. }
  184. }
  185. /**
  186. * @this {CompileContext}
  187. * @type {FromMarkdownHandle}
  188. */
  189. function enterMdxJsxTagSelfClosingMarker(token) {
  190. const tag = this.data.mdxJsxTag
  191. assert(tag, 'expected `mdxJsxTag`')
  192. if (tag.close) {
  193. throw new VFileMessage(
  194. 'Unexpected self-closing slash `/` in closing tag, expected the end of the tag',
  195. {start: token.start, end: token.end},
  196. 'mdast-util-mdx-jsx:unexpected-self-closing-slash'
  197. )
  198. }
  199. }
  200. /**
  201. * @this {CompileContext}
  202. * @type {FromMarkdownHandle}
  203. */
  204. function exitMdxJsxTagClosingMarker() {
  205. const tag = this.data.mdxJsxTag
  206. assert(tag, 'expected `mdxJsxTag`')
  207. tag.close = true
  208. }
  209. /**
  210. * @this {CompileContext}
  211. * @type {FromMarkdownHandle}
  212. */
  213. function exitMdxJsxTagNamePrimary(token) {
  214. const tag = this.data.mdxJsxTag
  215. assert(tag, 'expected `mdxJsxTag`')
  216. tag.name = this.sliceSerialize(token)
  217. }
  218. /**
  219. * @this {CompileContext}
  220. * @type {FromMarkdownHandle}
  221. */
  222. function exitMdxJsxTagNameMember(token) {
  223. const tag = this.data.mdxJsxTag
  224. assert(tag, 'expected `mdxJsxTag`')
  225. tag.name += '.' + this.sliceSerialize(token)
  226. }
  227. /**
  228. * @this {CompileContext}
  229. * @type {FromMarkdownHandle}
  230. */
  231. function exitMdxJsxTagNameLocal(token) {
  232. const tag = this.data.mdxJsxTag
  233. assert(tag, 'expected `mdxJsxTag`')
  234. tag.name += ':' + this.sliceSerialize(token)
  235. }
  236. /**
  237. * @this {CompileContext}
  238. * @type {FromMarkdownHandle}
  239. */
  240. function enterMdxJsxTagAttribute(token) {
  241. const tag = this.data.mdxJsxTag
  242. assert(tag, 'expected `mdxJsxTag`')
  243. enterMdxJsxTagAnyAttribute.call(this, token)
  244. tag.attributes.push({
  245. type: 'mdxJsxAttribute',
  246. name: '',
  247. value: null,
  248. position: {
  249. start: point(token.start),
  250. // @ts-expect-error: `end` will be patched later.
  251. end: undefined
  252. }
  253. })
  254. }
  255. /**
  256. * @this {CompileContext}
  257. * @type {FromMarkdownHandle}
  258. */
  259. function enterMdxJsxTagExpressionAttribute(token) {
  260. const tag = this.data.mdxJsxTag
  261. assert(tag, 'expected `mdxJsxTag`')
  262. enterMdxJsxTagAnyAttribute.call(this, token)
  263. tag.attributes.push({
  264. type: 'mdxJsxExpressionAttribute',
  265. value: '',
  266. position: {
  267. start: point(token.start),
  268. // @ts-expect-error: `end` will be patched later.
  269. end: undefined
  270. }
  271. })
  272. this.buffer()
  273. }
  274. /**
  275. * @this {CompileContext}
  276. * @type {FromMarkdownHandle}
  277. */
  278. function exitMdxJsxTagExpressionAttribute(token) {
  279. const tag = this.data.mdxJsxTag
  280. assert(tag, 'expected `mdxJsxTag`')
  281. const tail = tag.attributes[tag.attributes.length - 1]
  282. assert(tail.type === 'mdxJsxExpressionAttribute')
  283. const estree = token.estree
  284. tail.value = this.resume()
  285. assert(tail.position !== undefined)
  286. tail.position.end = point(token.end)
  287. if (estree) {
  288. tail.data = {estree}
  289. }
  290. }
  291. /**
  292. * @this {CompileContext}
  293. * @type {FromMarkdownHandle}
  294. */
  295. function exitMdxJsxTagAttributeNamePrimary(token) {
  296. const tag = this.data.mdxJsxTag
  297. assert(tag, 'expected `mdxJsxTag`')
  298. const node = tag.attributes[tag.attributes.length - 1]
  299. assert(node.type === 'mdxJsxAttribute')
  300. node.name = this.sliceSerialize(token)
  301. assert(node.position !== undefined)
  302. node.position.end = point(token.end)
  303. }
  304. /**
  305. * @this {CompileContext}
  306. * @type {FromMarkdownHandle}
  307. */
  308. function exitMdxJsxTagAttributeNameLocal(token) {
  309. const tag = this.data.mdxJsxTag
  310. assert(tag, 'expected `mdxJsxTag`')
  311. const node = tag.attributes[tag.attributes.length - 1]
  312. assert(node.type === 'mdxJsxAttribute')
  313. node.name += ':' + this.sliceSerialize(token)
  314. assert(node.position !== undefined)
  315. node.position.end = point(token.end)
  316. }
  317. /**
  318. * @this {CompileContext}
  319. * @type {FromMarkdownHandle}
  320. */
  321. function exitMdxJsxTagAttributeValueLiteral(token) {
  322. const tag = this.data.mdxJsxTag
  323. assert(tag, 'expected `mdxJsxTag`')
  324. const node = tag.attributes[tag.attributes.length - 1]
  325. node.value = parseEntities(this.resume(), {nonTerminated: false})
  326. assert(node.position !== undefined)
  327. node.position.end = point(token.end)
  328. }
  329. /**
  330. * @this {CompileContext}
  331. * @type {FromMarkdownHandle}
  332. */
  333. function exitMdxJsxTagAttributeValueExpression(token) {
  334. const tag = this.data.mdxJsxTag
  335. assert(tag, 'expected `mdxJsxTag`')
  336. const tail = tag.attributes[tag.attributes.length - 1]
  337. assert(tail.type === 'mdxJsxAttribute')
  338. /** @type {MdxJsxAttributeValueExpression} */
  339. const node = {type: 'mdxJsxAttributeValueExpression', value: this.resume()}
  340. const estree = token.estree
  341. if (estree) {
  342. node.data = {estree}
  343. }
  344. tail.value = node
  345. assert(tail.position !== undefined)
  346. tail.position.end = point(token.end)
  347. }
  348. /**
  349. * @this {CompileContext}
  350. * @type {FromMarkdownHandle}
  351. */
  352. function exitMdxJsxTagSelfClosingMarker() {
  353. const tag = this.data.mdxJsxTag
  354. assert(tag, 'expected `mdxJsxTag`')
  355. tag.selfClosing = true
  356. }
  357. /**
  358. * @this {CompileContext}
  359. * @type {FromMarkdownHandle}
  360. */
  361. function exitMdxJsxTag(token) {
  362. const tag = this.data.mdxJsxTag
  363. assert(tag, 'expected `mdxJsxTag`')
  364. const stack = this.data.mdxJsxTagStack
  365. assert(stack, 'expected `mdxJsxTagStack`')
  366. const tail = stack[stack.length - 1]
  367. if (tag.close && tail.name !== tag.name) {
  368. throw new VFileMessage(
  369. 'Unexpected closing tag `' +
  370. serializeAbbreviatedTag(tag) +
  371. '`, expected corresponding closing tag for `' +
  372. serializeAbbreviatedTag(tail) +
  373. '` (' +
  374. stringifyPosition(tail) +
  375. ')',
  376. {start: token.start, end: token.end},
  377. 'mdast-util-mdx-jsx:end-tag-mismatch'
  378. )
  379. }
  380. // End of a tag, so drop the buffer.
  381. this.resume()
  382. if (tag.close) {
  383. stack.pop()
  384. } else {
  385. this.enter(
  386. {
  387. type:
  388. token.type === 'mdxJsxTextTag'
  389. ? 'mdxJsxTextElement'
  390. : 'mdxJsxFlowElement',
  391. name: tag.name || null,
  392. attributes: tag.attributes,
  393. children: []
  394. },
  395. token,
  396. onErrorRightIsTag
  397. )
  398. }
  399. if (tag.selfClosing || tag.close) {
  400. this.exit(token, onErrorLeftIsTag)
  401. } else {
  402. stack.push(tag)
  403. }
  404. }
  405. /**
  406. * @this {CompileContext}
  407. * @type {OnEnterError}
  408. */
  409. function onErrorRightIsTag(closing, open) {
  410. const stack = this.data.mdxJsxTagStack
  411. assert(stack, 'expected `mdxJsxTagStack`')
  412. const tag = stack[stack.length - 1]
  413. assert(tag, 'expected `mdxJsxTag`')
  414. const place = closing ? ' before the end of `' + closing.type + '`' : ''
  415. const position = closing
  416. ? {start: closing.start, end: closing.end}
  417. : undefined
  418. throw new VFileMessage(
  419. 'Expected a closing tag for `' +
  420. serializeAbbreviatedTag(tag) +
  421. '` (' +
  422. stringifyPosition({start: open.start, end: open.end}) +
  423. ')' +
  424. place,
  425. position,
  426. 'mdast-util-mdx-jsx:end-tag-mismatch'
  427. )
  428. }
  429. /**
  430. * @this {CompileContext}
  431. * @type {OnExitError}
  432. */
  433. function onErrorLeftIsTag(a, b) {
  434. const tag = this.data.mdxJsxTag
  435. assert(tag, 'expected `mdxJsxTag`')
  436. throw new VFileMessage(
  437. 'Expected the closing tag `' +
  438. serializeAbbreviatedTag(tag) +
  439. '` either after the end of `' +
  440. b.type +
  441. '` (' +
  442. stringifyPosition(b.end) +
  443. ') or another opening tag after the start of `' +
  444. b.type +
  445. '` (' +
  446. stringifyPosition(b.start) +
  447. ')',
  448. {start: a.start, end: a.end},
  449. 'mdast-util-mdx-jsx:end-tag-mismatch'
  450. )
  451. }
  452. /**
  453. * Serialize a tag, excluding attributes.
  454. * `self-closing` is not supported, because we don’t need it yet.
  455. *
  456. * @param {Tag} tag
  457. * @returns {string}
  458. */
  459. function serializeAbbreviatedTag(tag) {
  460. return '<' + (tag.close ? '/' : '') + (tag.name || '') + '>'
  461. }
  462. }
  463. /**
  464. * Create an extension for `mdast-util-to-markdown` to enable MDX JSX.
  465. *
  466. * This extension configures `mdast-util-to-markdown` with
  467. * `options.fences: true` and `options.resourceLink: true` too, do not
  468. * overwrite them!
  469. *
  470. * @param {ToMarkdownOptions | null | undefined} [options]
  471. * Configuration (optional).
  472. * @returns {ToMarkdownExtension}
  473. * Extension for `mdast-util-to-markdown` to enable MDX JSX.
  474. */
  475. export function mdxJsxToMarkdown(options) {
  476. const options_ = options || {}
  477. const quote = options_.quote || '"'
  478. const quoteSmart = options_.quoteSmart || false
  479. const tightSelfClosing = options_.tightSelfClosing || false
  480. const printWidth = options_.printWidth || Number.POSITIVE_INFINITY
  481. const alternative = quote === '"' ? "'" : '"'
  482. if (quote !== '"' && quote !== "'") {
  483. throw new Error(
  484. 'Cannot serialize attribute values with `' +
  485. quote +
  486. '` for `options.quote`, expected `"`, or `\'`'
  487. )
  488. }
  489. mdxElement.peek = peekElement
  490. return {
  491. handlers: {
  492. mdxJsxFlowElement: mdxElement,
  493. mdxJsxTextElement: mdxElement
  494. },
  495. unsafe: [
  496. {character: '<', inConstruct: ['phrasing']},
  497. {atBreak: true, character: '<'}
  498. ],
  499. // Always generate fenced code (never indented code).
  500. fences: true,
  501. // Always generate links with resources (never autolinks).
  502. resourceLink: true
  503. }
  504. /**
  505. * @type {ToMarkdownHandle}
  506. * @param {MdxJsxFlowElement | MdxJsxTextElement} node
  507. */
  508. // eslint-disable-next-line complexity
  509. function mdxElement(node, _, state, info) {
  510. const flow = node.type === 'mdxJsxFlowElement'
  511. const selfClosing = node.name
  512. ? !node.children || node.children.length === 0
  513. : false
  514. const depth = inferDepth(state)
  515. const currentIndent = createIndent(depth)
  516. const trackerOneLine = state.createTracker(info)
  517. const trackerMultiLine = state.createTracker(info)
  518. /** @type {Array<string>} */
  519. const serializedAttributes = []
  520. const prefix = (flow ? currentIndent : '') + '<' + (node.name || '')
  521. const exit = state.enter(node.type)
  522. trackerOneLine.move(prefix)
  523. trackerMultiLine.move(prefix)
  524. // None.
  525. if (node.attributes && node.attributes.length > 0) {
  526. if (!node.name) {
  527. throw new Error('Cannot serialize fragment w/ attributes')
  528. }
  529. let index = -1
  530. while (++index < node.attributes.length) {
  531. const attribute = node.attributes[index]
  532. /** @type {string} */
  533. let result
  534. if (attribute.type === 'mdxJsxExpressionAttribute') {
  535. result = '{' + (attribute.value || '') + '}'
  536. } else {
  537. if (!attribute.name) {
  538. throw new Error('Cannot serialize attribute w/o name')
  539. }
  540. const value = attribute.value
  541. const left = attribute.name
  542. /** @type {string} */
  543. let right = ''
  544. if (value === null || value === undefined) {
  545. // Empty.
  546. } else if (typeof value === 'object') {
  547. right = '{' + (value.value || '') + '}'
  548. } else {
  549. // If the alternative is less common than `quote`, switch.
  550. const appliedQuote =
  551. quoteSmart && ccount(value, quote) > ccount(value, alternative)
  552. ? alternative
  553. : quote
  554. right =
  555. appliedQuote +
  556. stringifyEntitiesLight(value, {subset: [appliedQuote]}) +
  557. appliedQuote
  558. }
  559. result = left + (right ? '=' : '') + right
  560. }
  561. serializedAttributes.push(result)
  562. }
  563. }
  564. let attributesOnTheirOwnLine = false
  565. const attributesOnOneLine = serializedAttributes.join(' ')
  566. if (
  567. // Block:
  568. flow &&
  569. // Including a line ending (expressions).
  570. (/\r?\n|\r/.test(attributesOnOneLine) ||
  571. // Current position (including `<tag`).
  572. trackerOneLine.current().now.column +
  573. // -1 because columns, +1 for ` ` before attributes.
  574. // Attributes joined by spaces.
  575. attributesOnOneLine.length +
  576. // ` />`.
  577. (selfClosing ? (tightSelfClosing ? 2 : 3) : 1) >
  578. printWidth)
  579. ) {
  580. attributesOnTheirOwnLine = true
  581. }
  582. let tracker = trackerOneLine
  583. let value = prefix
  584. if (attributesOnTheirOwnLine) {
  585. tracker = trackerMultiLine
  586. let index = -1
  587. while (++index < serializedAttributes.length) {
  588. // Only indent first line of of attributes, we can’t indent attribute
  589. // values.
  590. serializedAttributes[index] =
  591. currentIndent + indent + serializedAttributes[index]
  592. }
  593. value += tracker.move(
  594. '\n' + serializedAttributes.join('\n') + '\n' + currentIndent
  595. )
  596. } else if (attributesOnOneLine) {
  597. value += tracker.move(' ' + attributesOnOneLine)
  598. }
  599. if (selfClosing) {
  600. value += tracker.move(
  601. (tightSelfClosing || attributesOnTheirOwnLine ? '' : ' ') + '/'
  602. )
  603. }
  604. value += tracker.move('>')
  605. if (node.children && node.children.length > 0) {
  606. if (node.type === 'mdxJsxTextElement') {
  607. value += tracker.move(
  608. state.containerPhrasing(node, {
  609. ...tracker.current(),
  610. before: '>',
  611. after: '<'
  612. })
  613. )
  614. } else {
  615. tracker.shift(2)
  616. value += tracker.move('\n')
  617. value += tracker.move(containerFlow(node, state, tracker.current()))
  618. value += tracker.move('\n')
  619. }
  620. }
  621. if (!selfClosing) {
  622. value += tracker.move(
  623. (flow ? currentIndent : '') + '</' + (node.name || '') + '>'
  624. )
  625. }
  626. exit()
  627. return value
  628. }
  629. }
  630. // Modified copy of:
  631. // <https://github.com/syntax-tree/mdast-util-to-markdown/blob/a381cbc/lib/util/container-flow.js>.
  632. //
  633. // To do: add `indent` support to `mdast-util-to-markdown`.
  634. // As indents are only used for JSX, it’s fine for now, but perhaps better
  635. // there.
  636. /**
  637. * @param {MdxJsxFlowElement} parent
  638. * Parent of flow nodes.
  639. * @param {State} state
  640. * Info passed around about the current state.
  641. * @param {ReturnType<Tracker['current']>} info
  642. * Info on where we are in the document we are generating.
  643. * @returns {string}
  644. * Serialized children, joined by (blank) lines.
  645. */
  646. function containerFlow(parent, state, info) {
  647. const indexStack = state.indexStack
  648. const children = parent.children
  649. const tracker = state.createTracker(info)
  650. const currentIndent = createIndent(inferDepth(state))
  651. /** @type {Array<string>} */
  652. const results = []
  653. let index = -1
  654. indexStack.push(-1)
  655. while (++index < children.length) {
  656. const child = children[index]
  657. indexStack[indexStack.length - 1] = index
  658. const childInfo = {before: '\n', after: '\n', ...tracker.current()}
  659. const result = state.handle(child, parent, state, childInfo)
  660. const serializedChild =
  661. child.type === 'mdxJsxFlowElement'
  662. ? result
  663. : state.indentLines(result, function (line, _, blank) {
  664. return (blank ? '' : currentIndent) + line
  665. })
  666. results.push(tracker.move(serializedChild))
  667. if (child.type !== 'list') {
  668. state.bulletLastUsed = undefined
  669. }
  670. if (index < children.length - 1) {
  671. results.push(tracker.move('\n\n'))
  672. }
  673. }
  674. indexStack.pop()
  675. return results.join('')
  676. }
  677. /**
  678. * @param {State} state
  679. * @returns {number}
  680. */
  681. function inferDepth(state) {
  682. let depth = 0
  683. let index = state.stack.length
  684. while (--index > -1) {
  685. const name = state.stack[index]
  686. if (name === 'blockquote' || name === 'listItem') break
  687. if (name === 'mdxJsxFlowElement') depth++
  688. }
  689. return depth
  690. }
  691. /**
  692. * @param {number} depth
  693. * @returns {string}
  694. */
  695. function createIndent(depth) {
  696. return indent.repeat(depth)
  697. }
  698. /**
  699. * @type {ToMarkdownHandle}
  700. */
  701. function peekElement() {
  702. return '<'
  703. }