mdx-get-headings.test.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { rehypeHeadingIds } from '@astrojs/markdown-remark';
  2. import mdx from '@astrojs/mdx';
  3. import { visit } from 'unist-util-visit';
  4. import { describe, it, before } from 'node:test';
  5. import * as assert from 'node:assert/strict';
  6. import { parseHTML } from 'linkedom';
  7. import { loadFixture } from '../../../astro/test/test-utils.js';
  8. describe('MDX getHeadings', () => {
  9. let fixture;
  10. before(async () => {
  11. fixture = await loadFixture({
  12. root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
  13. integrations: [mdx()],
  14. });
  15. await fixture.build();
  16. });
  17. it('adds anchor IDs to headings', async () => {
  18. const html = await fixture.readFile('/test/index.html');
  19. const { document } = parseHTML(html);
  20. const h2Ids = document.querySelectorAll('h2').map((el) => el?.id);
  21. const h3Ids = document.querySelectorAll('h3').map((el) => el?.id);
  22. assert.equal(document.querySelector('h1').id, 'heading-test');
  23. assert.equal(h2Ids.includes('section-1'), true);
  24. assert.equal(h2Ids.includes('section-2'), true);
  25. assert.equal(h3Ids.includes('subsection-1'), true);
  26. assert.equal(h3Ids.includes('subsection-2'), true);
  27. });
  28. it('generates correct getHeadings() export', async () => {
  29. const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
  30. // TODO: make this a snapshot test :)
  31. assert.equal(
  32. JSON.stringify(headingsByPage['./test.mdx']),
  33. JSON.stringify([
  34. { depth: 1, slug: 'heading-test', text: 'Heading test' },
  35. { depth: 2, slug: 'section-1', text: 'Section 1' },
  36. { depth: 3, slug: 'subsection-1', text: 'Subsection 1' },
  37. { depth: 3, slug: 'subsection-2', text: 'Subsection 2' },
  38. { depth: 2, slug: 'section-2', text: 'Section 2' },
  39. ])
  40. );
  41. });
  42. it('generates correct getHeadings() export for JSX expressions', async () => {
  43. const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
  44. assert.equal(
  45. JSON.stringify(headingsByPage['./test-with-jsx-expressions.mdx']),
  46. JSON.stringify([
  47. {
  48. depth: 1,
  49. slug: 'heading-test-with-jsx-expressions',
  50. text: 'Heading test with JSX expressions',
  51. },
  52. { depth: 2, slug: 'h2title', text: 'h2Title' },
  53. { depth: 3, slug: 'h3title', text: 'h3Title' },
  54. ])
  55. );
  56. });
  57. });
  58. describe('MDX heading IDs can be customized by user plugins', () => {
  59. let fixture;
  60. before(async () => {
  61. fixture = await loadFixture({
  62. root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
  63. integrations: [mdx()],
  64. markdown: {
  65. rehypePlugins: [
  66. () => (tree) => {
  67. let count = 0;
  68. visit(tree, 'element', (node) => {
  69. if (!/^h\d$/.test(node.tagName)) return;
  70. if (!node.properties?.id) {
  71. node.properties = { ...node.properties, id: String(count++) };
  72. }
  73. });
  74. },
  75. ],
  76. },
  77. });
  78. await fixture.build();
  79. });
  80. it('adds user-specified IDs to HTML output', async () => {
  81. const html = await fixture.readFile('/test/index.html');
  82. const { document } = parseHTML(html);
  83. const h1 = document.querySelector('h1');
  84. assert.equal(h1?.textContent, 'Heading test');
  85. assert.equal(h1?.getAttribute('id'), '0');
  86. const headingIDs = document.querySelectorAll('h1,h2,h3').map((el) => el.id);
  87. assert.equal(
  88. JSON.stringify(headingIDs),
  89. JSON.stringify(Array.from({ length: headingIDs.length }, (_, idx) => String(idx)))
  90. );
  91. });
  92. it('generates correct getHeadings() export', async () => {
  93. const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
  94. assert.equal(
  95. JSON.stringify(headingsByPage['./test.mdx']),
  96. JSON.stringify([
  97. { depth: 1, slug: '0', text: 'Heading test' },
  98. { depth: 2, slug: '1', text: 'Section 1' },
  99. { depth: 3, slug: '2', text: 'Subsection 1' },
  100. { depth: 3, slug: '3', text: 'Subsection 2' },
  101. { depth: 2, slug: '4', text: 'Section 2' },
  102. ])
  103. );
  104. });
  105. });
  106. describe('MDX heading IDs can be injected before user plugins', () => {
  107. let fixture;
  108. before(async () => {
  109. fixture = await loadFixture({
  110. root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
  111. integrations: [
  112. mdx({
  113. rehypePlugins: [
  114. rehypeHeadingIds,
  115. () => (tree) => {
  116. visit(tree, 'element', (node) => {
  117. if (!/^h\d$/.test(node.tagName)) return;
  118. if (node.properties?.id) {
  119. node.children.push({ type: 'text', value: ' ' + node.properties.id });
  120. }
  121. });
  122. },
  123. ],
  124. }),
  125. ],
  126. });
  127. await fixture.build();
  128. });
  129. it('adds user-specified IDs to HTML output', async () => {
  130. const html = await fixture.readFile('/test/index.html');
  131. const { document } = parseHTML(html);
  132. const h1 = document.querySelector('h1');
  133. assert.equal(h1?.textContent, 'Heading test heading-test');
  134. assert.equal(h1?.id, 'heading-test');
  135. });
  136. });
  137. describe('MDX headings with frontmatter', () => {
  138. let fixture;
  139. before(async () => {
  140. fixture = await loadFixture({
  141. root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
  142. integrations: [mdx()],
  143. });
  144. await fixture.build();
  145. });
  146. it('adds anchor IDs to headings', async () => {
  147. const html = await fixture.readFile('/test-with-frontmatter/index.html');
  148. const { document } = parseHTML(html);
  149. const h3Ids = document.querySelectorAll('h3').map((el) => el?.id);
  150. assert.equal(document.querySelector('h1').id, 'the-frontmatter-title');
  151. assert.equal(document.querySelector('h2').id, 'frontmattertitle');
  152. assert.equal(h3Ids.includes('keyword-2'), true);
  153. assert.equal(h3Ids.includes('tag-1'), true);
  154. assert.equal(document.querySelector('h4').id, 'item-2');
  155. assert.equal(document.querySelector('h5').id, 'nested-item-3');
  156. assert.equal(document.querySelector('h6').id, 'frontmatterunknown');
  157. });
  158. it('generates correct getHeadings() export', async () => {
  159. const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
  160. assert.equal(
  161. JSON.stringify(headingsByPage['./test-with-frontmatter.mdx']),
  162. JSON.stringify([
  163. { depth: 1, slug: 'the-frontmatter-title', text: 'The Frontmatter Title' },
  164. { depth: 2, slug: 'frontmattertitle', text: 'frontmatter.title' },
  165. { depth: 3, slug: 'keyword-2', text: 'Keyword 2' },
  166. { depth: 3, slug: 'tag-1', text: 'Tag 1' },
  167. { depth: 4, slug: 'item-2', text: 'Item 2' },
  168. { depth: 5, slug: 'nested-item-3', text: 'Nested Item 3' },
  169. { depth: 6, slug: 'frontmatterunknown', text: 'frontmatter.unknown' },
  170. ])
  171. );
  172. });
  173. });