diff --git a/__tests__/helpers.ts b/__tests__/helpers.ts index 5d4e16b44..621ef035a 100644 --- a/__tests__/helpers.ts +++ b/__tests__/helpers.ts @@ -1,9 +1,60 @@ +import type { Element, Root, RootContent } from 'hast'; + import * as rdmd from '@readme/markdown-legacy'; import { vi } from 'vitest'; import { run, compile, migrate as baseMigrate, mdastV6 } from '../index'; +import { type RMDXModule } from '../types'; + +/** Recursively searches a hast tree and returns the first element matching the given tag name. */ +export function findElementByTagName(node: Root | RootContent, tagName: string): Element | null { + if ('type' in node && node.type === 'element' && 'tagName' in node && node.tagName === tagName) { + return node; + } + + if ('children' in node && Array.isArray(node.children)) { + return node.children.reduce((found, child) => { + if (found) return found; + return findElementByTagName(child, tagName); + }, null); + } + + return null; +} + +/** Recursively searches a hast tree and returns all elements matching the given tag name. */ +export function findElementsByTagName(node: Root | RootContent, tagName: string): Element[] { + const results: Element[] = []; + + if ('type' in node && node.type === 'element' && 'tagName' in node && node.tagName === tagName) { + results.push(node); + } + + if ('children' in node && Array.isArray(node.children)) { + node.children.forEach(child => { + results.push(...findElementsByTagName(child, tagName)); + }); + } + + return results; +} + +/** A minimal stub satisfying the RMDXModule interface. Use when tests need a component module without real rendering. */ +export const stubModule = { + default: () => null as unknown as React.JSX.Element, + Toc: null, + toc: [], +} satisfies RMDXModule; + +/** Builds a component map from a list of names, each mapped to `stubModule`. Use to mock the components option in compile/run. */ +export const makeComponents = (...names: string[]) => + names.reduce>((acc, name) => { + acc[name] = stubModule; + return acc; + }, {}); +/** Temporarily suppresses console output for the given method (default: `error`) while executing a callback. Restores the original implementation afterward. */ export const silenceConsole = (prop: keyof Console = 'error', impl = () => {}) => fn => { @@ -18,6 +69,7 @@ export const silenceConsole = } }; +/** Compiles and runs a markdown string through the full mdx pipeline. Returns the default export (React component) unless `getDefault` is false, in which case the full module is returned. */ export const execute = (doc: string, compileOpts = {}, runOpts = {}, { getDefault = true } = {}) => { const code = compile(doc, compileOpts); const mod = run(code, runOpts); @@ -25,10 +77,12 @@ export const execute = (doc: string, compileOpts = {}, runOpts = {}, { getDefaul return getDefault ? mod.default : mod; }; +/** Runs the legacy-to-v7 migration on a markdown string using the legacy rdmd parser. */ export const migrate = (doc: string) => { return baseMigrate(doc, { rdmd }); }; +/** Parses a markdown string into an mdast tree using the v6-compatible parser with legacy rdmd support. */ export const mdastV6Wrapper = (doc: string) => { return mdastV6(doc, { rdmd }); }; diff --git a/__tests__/lib/mdxish/demo-docs/mdxish.md b/__tests__/lib/mdxish/demo-docs/mdxish.md deleted file mode 100644 index 780ad7cfd..000000000 --- a/__tests__/lib/mdxish/demo-docs/mdxish.md +++ /dev/null @@ -1,121 +0,0 @@ -# MDX-ish Engine (Proposed Loose MDX-Like Syntax) - -A demo doc for the proposed loose "MDX-ish" syntax. Test against this doc (as well as the legacy RDMD and new RMDX docs) to validate that the engine can parse and render our new mixed content syntax. - -## Mixed HTML Content - -
-

This is an HTML Section

-

You can mix HTML directly into your markdown content.

- This is an orange span element! -
- -Regular markdown continues after HTML elements without any issues.You can even write loose html, so unclosed tags like `
` or `
` will work! - -
- -HTML comment blocks should also work without issue. - -## Custom Components - -Custom components and reusable content should be fully supported: - - - -Lorem ipsum dolor sit amet, **consectetur adipiscing elit.** Ut enim ad minim veniam, quis nostrud exercitation ullamco. Excepteur sint occaecat cupidatat non proident! - - - -You should be able to use our built in components as if they were globals. Here's our "Run in Postman" button, for example: - - - -### Component Composition - -You can nest components inside each other! Here's an `` nested inside a ``, for example: - - - - -This Accordion is nested inside a Card component! - - - - -## Mixed Attribute Syntax - -### Style - -
- -You can use a JSX-style CSS object to set inline styles. - -
- -
- -Or use the standard HTML `[style]` attribute. - -
- -### Class - -
- -Using the `className` attribute. - -
- -
- -Or just the regular HTML `class` attribute - -
- - - -## Limited Top-Level JSX - -- Logic: **`{3 * 7 + 11}`** evaluates to {3 * 7 + 11} -- Global Methods: **`{uppercase('hello world')}`** evaluates to {uppercase('hello world')} -- User Variables: **`{user.name}`** evaluates to {user.name} -- Comments: **`{/* JSX-style comments */}`** should not render {/* this should not be rendered */} - -## Mixed MD & JSX Syntax - -- Inline decorators should work with top-level JSX expressions. For example: - - > **{count}** items at _${price}_ is [${Math.round(multiply(count, price))}](https://google.com). - -- Attributes can be given as plain HTML or as a JSX expression, so `` and `` should both work: - - > an plain HTML attr versus a JSX expression - - -### Code Blocks Should NOT Execute - -Both inline code + code blocks should preserve expressions, instead of evaluating them: - -```javascript -const result = {1 + 1}; -const user = {userName}; -const math = {5 * 10}; -``` - -Inline code also shouldn't evaluate: `{1 + 1}` should stay as-is in inline code. diff --git a/__tests__/lib/mdxish/demo-docs/rdmd.md b/__tests__/lib/mdxish/demo-docs/rdmd.md deleted file mode 100644 index d4ff06c02..000000000 --- a/__tests__/lib/mdxish/demo-docs/rdmd.md +++ /dev/null @@ -1,79 +0,0 @@ -## RDMD Engine (Legacy Markdown) - -A comprehensive demo of ReadMe's legacy RDMD flavored Markdown syntax. Test against this doc to validate that legacy RDMD content is rendering properly. - -### Reusable Content - - - -### Code Blocks - -RDMD renders all standard markdown codeblocks. Additionally, when using fenced codeblocks, you can provide an optional title for your block after the syntax lang tag: - -```php Sample Code - -``` - -RDMD can display multiple code samples in a tabbed interface. To create tabs, write successive fenced code blocks **without** inserting an empty line between blocks. For example: - -```js Tab One -console.log('Code Tab A'); -``` -```python Tab Two -print('Code Tab B') -``` - -The engine should render the above code blocks as a set of tabs. - -### Callouts - -A callout is a special blockquote that begins with either the ℹ️, ✅, ⚠️, or ❗️ emoji. This initial emoji will set the callout’s theme, and the first line becomes the title. For instance: - -> ✅ Callout Title -> -> This should render a success callout. - -This creates a success callout. Some edge cases are also covered, such as title-only callouts: - -> ℹ️ Callouts don't need to have body text. - -Nor do they require a title, or a double line break between title and body: - -> ⚠️ -> This callout has a title but no body text. - -Finally, if an emoji that isn’t mapped to a theme is used, the callout will fall back to a default style. To prevent a regular blockquote starting with one of the theme emojis from rendering as a callout, you can simply bold the leading emoji in the quote: - -> **❗️** This should render a regular blockquote, not a callout. - -### Embeds - -RDMD supports rich embeds. You can embed a URL with a special title `@embed` in a normal Markdown link. So for example, this `[Embed Title](https://youtu.be/8bh238ekw3 "@embed")` syntax should render a "rich" preview: - -[Embed Title](https://youtu.be/8bh238ekw3 "@embed") - -For more control, use the `` JSX component and pass properties such as `url`, `title`, `favicon` and `image`. - - -### Dynamic Data - -RDMD can substitute variables and glossary terms at render time: - -* **User variables:** if JWT‑based user variables are configured, you can reference them using curly braces. For example, “`Hi, my name is **<>**!`” expands to the logged‑in user’s name: - - > Hi, my name is **<>**! - -* **Glossary terms:** similarly, if you have defined any glossary terms, you can use the `<>` to show an interactive definition tooltip. - - > The term <> should show a tooltip on hover. - -* **Emoji shortcodes:** GitHub‑style emoji short codes like `:sparkles:` or `:owlbert-reading:` are expanded to their corresponding emoji or custom image. - -### Additional Features - -- automatic table of contents (TOC) generation per doc section -- Mermaid syntax support for rendering diagrams -- heading semantics + syntax variants: - * auto‑incremented anchor IDs applied to headings for jump link support - * supports compact style, so you can omit the space after the hash, i.e. `###Valid Header` - * respects ATX style headings, so you can wrap headings in hashes, e.g. `## Valid Header ##` diff --git a/__tests__/lib/mdxish/demo-docs/rmdx.md b/__tests__/lib/mdxish/demo-docs/rmdx.md deleted file mode 100644 index 295ed1391..000000000 --- a/__tests__/lib/mdxish/demo-docs/rmdx.md +++ /dev/null @@ -1,108 +0,0 @@ -# RMDX Engine (Refactored MDX) - -A comprehensive demo of ReadMe's current MDX Markdown syntax. Test against this doc to validate that legacy RDMD content is rendering properly. - -### Reusable Content - -Project custom components should be provided to the engine at render time and be usable in the doc: - -Hello world! - -Reusable content should work the same way: - - - -### Code Blocks - -RDMD renders all standard markdown codeblocks. Additionally, when using fenced codeblocks, you can provide an optional title for your block after the syntax lang tag: - -```php Sample Code - -``` - -RDMD can display multiple code samples in a tabbed interface. To create tabs, write successive fenced code blocks **without** inserting an empty line between blocks. For example: - -```js Tab One -console.log('Code Tab A'); -``` -```python Tab Two -print('Code Tab B') -``` - -The engine should render the above code blocks as a set of tabs. - -### Callouts - -A callout is a special blockquote that begins with either the ℹ️, ✅, ⚠️, or ❗️ emoji. This initial emoji will set the callout’s theme, and the first line becomes the title. For instance: - -> ✅ Callout Title -> -> This should render a success callout. - -This creates a success callout. Some edge cases are also covered, such as title-only callouts: - -> ℹ️ Callouts don't need to have body text. - -Nor do they require a title, or a double line break between title and body: - -> ⚠️ -> This callout has a title but no body text. - -Finally, if an emoji that isn’t mapped to a theme is used, the callout will fall back to a default style. Callouts can also be written using our custom `` component, which accepts a separate `icon` and `theme` prop for even more flexibility. This should render similarly to the above examples: - - -### Callout Component - -A default callout using the MDX component. - - -To prevent a regular blockquote starting with one of the theme emojis from rendering as a callout, you can simply bold the leading emoji in the quote: - -> **❗️** This should render a regular blockquote, not an error callout. - -### Embeds - -RDMD supports rich embeds. You can embed a URL with a special title `@embed` in a normal Markdown link. So for example, this `[Embed Title](https://youtu.be/8bh238ekw3 "@embed")` syntax should render a "rich" preview: - -[Embed Title](https://youtu.be/8bh238ekw3 "@embed") - -For more control, use the `` JSX component and pass properties such as `url`, `title`, `favicon` and `image`. - - - -### Dynamic Data - -RDMD can substitute variables and glossary terms at render time: - -* **User variables:** if JWT‑based user variables are configured, you can reference them using curly braces. For example, “`Hi, my name is **{user.name}**!`” expands to the logged‑in user’s name: - - > Hi, my name is **{user.name}**! - -* **Glossary terms:** similarly, if you have defined any glossary terms, you can use the `myterm` tag to show an interactive definition tooltip: - - > The term exogenous should show a tooltip on hover. - -* **Emoji shortcodes:** GitHub‑style emoji short codes like `:sparkles:` or `:owlbert-reading:` are expanded to their corresponding emoji or custom image. - -### Top-Level JSX Syntax - -- top-level logic can be written as JSX **`{3 * 7 + 11}`** expressions and should evaluate inline (to {3 * 7 + 11} in this case.) -- global JS methods are supported, such as **`{uppercase('hello world')}`** (which should evaluate to {uppercase('hello world')}.) -- JSX comments like **`{/* JSX-style comments */}`** should work (while HTML comments like `` will throw an error.) -- JSX special attributes (like `className`, or setting the `style` as a CSS object) are required -- loose HTML is not supported (i.e. unclosed `
` tags will throw an error) - -### Additional Features - -- automatic table of contents (TOC) generation per doc section -- Mermaid syntax support for rendering diagrams -- heading semantics + syntax variants: - * auto‑incremented anchor IDs applied to headings for jump link support - * supports compact style, so you can omit the space after the hash, i.e. `###Valid Header` - * respects ATX style headings, so you can wrap headings in hashes, e.g. `## Valid Header ##` diff --git a/__tests__/lib/mdxish/legacy-variables.test.ts b/__tests__/lib/mdxish/legacy-variables.test.ts index b25833b15..338eb713f 100644 --- a/__tests__/lib/mdxish/legacy-variables.test.ts +++ b/__tests__/lib/mdxish/legacy-variables.test.ts @@ -2,22 +2,7 @@ import type { CustomComponents } from '../../../types'; import type { Element, Root, Text } from 'hast'; import { mdxish } from '../../../lib'; - -function findElementByTagName(tree: Element, tagName: string): Element | null { - if (tree.tagName === tagName) { - return tree; - } - // Recursively search children - let result: Element | null = null; - tree.children.some(child => { - if ('tagName' in child) { - result = findElementByTagName(child as Element, tagName); - return result !== null; - } - return false; - }); - return result; -} +import { findElementByTagName } from '../../helpers'; // Count all variable nodes in the tree (recursively) function countVariableNodes(node: Element | Root): number { diff --git a/__tests__/lib/mdxish/mdx-expressions.test.ts b/__tests__/lib/mdxish/mdx-expressions.test.ts index 924592fec..628396b87 100644 --- a/__tests__/lib/mdxish/mdx-expressions.test.ts +++ b/__tests__/lib/mdxish/mdx-expressions.test.ts @@ -1,8 +1,6 @@ import type { Element, Text } from 'hast'; -import { describe, it, expect } from 'vitest'; - -import { mdxish } from '../../../lib/mdxish'; +import { mdxish } from '../../../lib'; describe('mdxish MDX expressions', () => { describe('inline expressions', () => { @@ -388,9 +386,7 @@ Result: {result}`; const ast = mdxish(md, { jsxContext: { value: 'middle' } }); // Should have parsed the magic block correctly - const codeTabs = ast.children.find( - c => (c as Element).tagName === 'CodeTabs', - ) as Element; + const codeTabs = ast.children.find(c => (c as Element).tagName === 'CodeTabs') as Element; expect(codeTabs).toBeDefined(); }); }); diff --git a/__tests__/lib/mdxish/mdxishAstProcessor.test.ts b/__tests__/lib/mdxish/mdxish-ast-processor.test.ts similarity index 87% rename from __tests__/lib/mdxish/mdxishAstProcessor.test.ts rename to __tests__/lib/mdxish/mdxish-ast-processor.test.ts index b2b4cb30e..e6b07ce26 100644 --- a/__tests__/lib/mdxish/mdxishAstProcessor.test.ts +++ b/__tests__/lib/mdxish/mdxish-ast-processor.test.ts @@ -1,6 +1,6 @@ import type { Root } from 'mdast'; -import { mdxishAstProcessor } from '../../../lib/mdxish'; +import { mdxishAstProcessor } from '../../../lib'; describe('mdxishAstProcessor', () => { describe('deferred processing (handled by mdxish rendering pipeline)', () => { @@ -109,6 +109,24 @@ describe('mdxishAstProcessor', () => { }); }); + describe('with safeMode', () => { + it('should not include mdxExpression extensions in safeMode', () => { + const md = 'Test {expression}'; + const { processor } = mdxishAstProcessor(md, { safeMode: true }); + const mdast = processor.parse(md); + const hasMdxExpression = JSON.stringify(mdast).includes('mdxTextExpression'); + expect(hasMdxExpression).toBe(false); + }); + + it('should include mdxExpression extensions without safeMode', () => { + const md = 'Test {expression}'; + const { processor, parserReadyContent } = mdxishAstProcessor(md, { safeMode: false }); + const mdast = processor.parse(parserReadyContent); + const hasMdxExpression = JSON.stringify(mdast).includes('mdxTextExpression'); + expect(hasMdxExpression).toBe(true); + }); + }); + it('should convert GFM checklist nodes and retain checkboxes that have no text after them', () => { const md = `- [ ] hi - [ ] `; diff --git a/__tests__/lib/mdxish/mdxish-jsx-to-mdast.test.ts b/__tests__/lib/mdxish/mdxish-jsx-to-mdast.test.ts index 4bf53d6fa..8c940225c 100644 --- a/__tests__/lib/mdxish/mdxish-jsx-to-mdast.test.ts +++ b/__tests__/lib/mdxish/mdxish-jsx-to-mdast.test.ts @@ -2,7 +2,7 @@ import type { Anchor, Callout, EmbedBlock, ImageBlock, Recipe } from '../../../t import type { Paragraph, Root } from 'mdast'; import { NodeTypes } from '../../../enums'; -import { mdxishAstProcessor } from '../../../lib/mdxish'; +import { mdxishAstProcessor } from '../../../lib'; describe('mdxish-jsx-to-mdast transformer', () => { describe('with newEditorTypes enabled', () => { diff --git a/__tests__/lib/mdxish/mdxishMdastToMd.test.ts b/__tests__/lib/mdxish/mdxish-mdast-to-md.test.ts similarity index 100% rename from __tests__/lib/mdxish/mdxishMdastToMd.test.ts rename to __tests__/lib/mdxish/mdxish-mdast-to-md.test.ts diff --git a/__tests__/lib/mdxish/mdxish-snake-case.test.ts b/__tests__/lib/mdxish/mdxish-snake-case.test.ts index a7f2fb589..6234082f1 100644 --- a/__tests__/lib/mdxish/mdxish-snake-case.test.ts +++ b/__tests__/lib/mdxish/mdxish-snake-case.test.ts @@ -1,19 +1,7 @@ import type { Element } from 'hast'; -import { mdxish } from '../../../lib/mdxish'; -import { type RMDXModule } from '../../../types'; - -const stubModule: RMDXModule = { - default: () => null, - Toc: null, - toc: [], -}; - -const makeComponents = (...names: string[]) => - names.reduce>((acc, name) => { - acc[name] = stubModule; - return acc; - }, {}); +import { mdxish } from '../../../lib'; +import { makeComponents } from '../../helpers'; describe('mdxish snake_case component integration', () => { describe('basic rendering', () => { diff --git a/__tests__/lib/mdxishTags.test.ts b/__tests__/lib/mdxish/mdxish-tags.test.ts similarity index 95% rename from __tests__/lib/mdxishTags.test.ts rename to __tests__/lib/mdxish/mdxish-tags.test.ts index 5df5f3df7..2b0b8e20f 100644 --- a/__tests__/lib/mdxishTags.test.ts +++ b/__tests__/lib/mdxish/mdxish-tags.test.ts @@ -1,4 +1,4 @@ -import { mdxishTags } from '../../lib'; +import { mdxishTags } from '../../../lib'; describe('mdxishTags', () => { it('returns custom element names', () => { diff --git a/__tests__/lib/mdxish/mdxish.test.ts b/__tests__/lib/mdxish/mdxish.test.ts index 25db92590..e4138f74e 100644 --- a/__tests__/lib/mdxish/mdxish.test.ts +++ b/__tests__/lib/mdxish/mdxish.test.ts @@ -1,26 +1,9 @@ import type { CustomComponents } from '../../../types'; -import type { Element, Root, RootContent, Text } from 'hast'; +import type { Element, Text } from 'hast'; -import { mdxish, mdxishAstProcessor } from '../../../lib/mdxish'; +import { mdxish } from '../../../lib'; import { extractText } from '../../../processor/transform/extract-text'; - -type HastNode = Root | RootContent; - -/** - * Recursively finds an element with the specified tagName in a HAST tree. - */ -function findElementByTagName(node: HastNode, tagName: string): Element | null { - if ('type' in node && node.type === 'element' && 'tagName' in node && node.tagName === tagName) { - return node; - } - if ('children' in node && Array.isArray(node.children)) { - return node.children.reduce((found, child) => { - if (found) return found; - return findElementByTagName(child, tagName); - }, null); - } - return null; -} +import { findElementByTagName } from '../../helpers'; describe('mdxish should render', () => { describe('invalid mdx syntax', () => { @@ -212,23 +195,6 @@ describe('mdxish safeMode', () => { }); }); - describe('mdxishAstProcessor with safeMode', () => { - it('should not include mdxExpression extensions in safeMode', () => { - const md = 'Test {expression}'; - const { processor } = mdxishAstProcessor(md, { safeMode: true }); - const mdast = processor.parse(md); - const hasMdxExpression = JSON.stringify(mdast).includes('mdxTextExpression'); - expect(hasMdxExpression).toBe(false); - }); - - it('should include mdxExpression extensions without safeMode', () => { - const md = 'Test {expression}'; - const { processor, parserReadyContent } = mdxishAstProcessor(md, { safeMode: false }); - const mdast = processor.parse(parserReadyContent); - const hasMdxExpression = JSON.stringify(mdast).includes('mdxTextExpression'); - expect(hasMdxExpression).toBe(true); - }); - }); }); describe('HTML flow block termination', () => { diff --git a/__tests__/lib/mdxish/magic-block-table-perf.test.ts b/__tests__/lib/mdxish/perf/magic-block-table-perf.test.ts similarity index 98% rename from __tests__/lib/mdxish/magic-block-table-perf.test.ts rename to __tests__/lib/mdxish/perf/magic-block-table-perf.test.ts index 475f9803e..887075c16 100644 --- a/__tests__/lib/mdxish/magic-block-table-perf.test.ts +++ b/__tests__/lib/mdxish/perf/magic-block-table-perf.test.ts @@ -4,7 +4,7 @@ * Validates that large tables with complex cell content * (markdown, HTML, variables) parse within reasonable time. */ -import { mdxish } from '../../../lib'; +import { mdxish } from '../../../../lib'; function buildTableBlock( rows: number, diff --git a/__tests__/lib/mdxish/tailwind.test.ts b/__tests__/lib/mdxish/tailwind.test.ts index bb7c6a971..ebf70a325 100644 --- a/__tests__/lib/mdxish/tailwind.test.ts +++ b/__tests__/lib/mdxish/tailwind.test.ts @@ -1,37 +1,5 @@ -import type { Element, Root } from 'hast'; -import type { MDXProps } from 'mdx/types'; - -import React from 'react'; - import { mdxish } from '../../../lib'; -import { type RMDXModule } from '../../../types'; - -/** - * Helper to find all elements with a specific tag name in the HAST tree. - */ -const findElementsByTagName = (node: Element | Root, tagName: string): Element[] => { - const results: Element[] = []; - - if (node.type === 'element' && node.tagName === tagName) { - results.push(node); - } - - if ('children' in node && Array.isArray(node.children)) { - node.children.forEach(child => { - if (child.type === 'element') { - results.push(...findElementsByTagName(child, tagName)); - } - }); - } - - return results; -}; - -const stubModule = (component: React.FC): RMDXModule => ({ - default: component as RMDXModule['default'], - Toc: null, - toc: [], -}); +import { findElementsByTagName, stubModule } from '../../helpers'; describe('mdxish tailwind transformer', () => { describe('when useTailwind is not set', () => { @@ -69,11 +37,8 @@ describe('mdxish tailwind transformer', () => { describe('passed-in custom components', () => { it('should wrap custom component with TailwindRoot', () => { - const CustomButton: React.FC = props => - React.createElement('button', { className: 'custom-btn' }, props.children as React.ReactNode); - const components = { - CustomButton: stubModule(CustomButton), + CustomButton: stubModule, }; const md = 'Click me'; diff --git a/__tests__/lib/mdxish/variables-code.test.ts b/__tests__/lib/mdxish/variables-code.test.ts index 92658a8ad..e37c1a672 100644 --- a/__tests__/lib/mdxish/variables-code.test.ts +++ b/__tests__/lib/mdxish/variables-code.test.ts @@ -1,23 +1,7 @@ -import type { Element, Root, RootContent, Text } from 'hast'; +import type { Root, Text } from 'hast'; import { mdxish } from '../../../lib'; - -type HastNode = Root | RootContent; - -function findElementByTagName(node: HastNode, tagName: string): Element | null { - if ('type' in node && node.type === 'element' && 'tagName' in node && node.tagName === tagName) { - return node; - } - - if ('children' in node && Array.isArray(node.children)) { - return node.children.reduce((found, child) => { - if (found) return found; - return findElementByTagName(child, tagName); - }, null); - } - - return null; -} +import { findElementByTagName } from '../../helpers'; function getCodeText(tree: Root): string { const code = findElementByTagName(tree, 'code'); diff --git a/__tests__/processor/plugin/mdxish-components.test.ts b/__tests__/processor/plugin/mdxish-components.test.ts index 4e075446c..c89cec420 100644 --- a/__tests__/processor/plugin/mdxish-components.test.ts +++ b/__tests__/processor/plugin/mdxish-components.test.ts @@ -1,9 +1,7 @@ import type { CustomComponents } from '../../../types'; -import type { Element, Root, RootContent } from 'hast'; - -import { describe, it, expect } from 'vitest'; import { mix, mdxish } from '../../../lib'; +import { findElementsByTagName } from '../../helpers'; describe('rehypeMdxishComponents', () => { it('should remove non-existent custom components from the tree', () => { @@ -94,26 +92,6 @@ hello * Fix: Changed regex from case-insensitive ('gi') to case-sensitive ('g') */ describe('smartCamelCase (prop normalization)', () => { - // Helper to find elements by tagName in HAST tree - function findElementsByTagName(tree: Root | RootContent, tagName: string): Element[] { - const elements: Element[] = []; - - if ('type' in tree && tree.type === 'element') { - const elem = tree as Element; - if (elem.tagName.toLowerCase() === tagName.toLowerCase()) { - elements.push(elem); - } - } - - if ('children' in tree && Array.isArray(tree.children)) { - tree.children.forEach(child => { - elements.push(...findElementsByTagName(child as RootContent, tagName)); - }); - } - - return elements; - } - it('should preserve iconColor prop casing', () => { const TestComponent = {} as CustomComponents[string]; const markdown = ''; diff --git a/__tests__/transformers/evaluate-expressions.test.ts b/__tests__/transformers/mdxish/evaluate-expressions.test.ts similarity index 98% rename from __tests__/transformers/evaluate-expressions.test.ts rename to __tests__/transformers/mdxish/evaluate-expressions.test.ts index 1fef2271c..836e276cd 100644 --- a/__tests__/transformers/evaluate-expressions.test.ts +++ b/__tests__/transformers/mdxish/evaluate-expressions.test.ts @@ -1,4 +1,4 @@ -import { mix } from '../../lib'; +import { mix } from '../../../lib'; describe('evaluateExpressions', () => { it('should evaluate numeric operations', () => { diff --git a/__tests__/lib/mdxish/gemoji.test.ts b/__tests__/transformers/mdxish/gemoji.test.ts similarity index 99% rename from __tests__/lib/mdxish/gemoji.test.ts rename to __tests__/transformers/mdxish/gemoji.test.ts index ebef90679..02bf9d467 100644 --- a/__tests__/lib/mdxish/gemoji.test.ts +++ b/__tests__/transformers/mdxish/gemoji.test.ts @@ -13,6 +13,5 @@ describe('gemoji transformer', () => {

😃

:owlbert:

" `); - }); -}); \ No newline at end of file +}); diff --git a/__tests__/transformers/mdxish-component-blocks.test.ts b/__tests__/transformers/mdxish/mdxish-component-blocks.test.ts similarity index 99% rename from __tests__/transformers/mdxish-component-blocks.test.ts rename to __tests__/transformers/mdxish/mdxish-component-blocks.test.ts index 802765fd0..acf839ed6 100644 --- a/__tests__/transformers/mdxish-component-blocks.test.ts +++ b/__tests__/transformers/mdxish/mdxish-component-blocks.test.ts @@ -3,7 +3,7 @@ import type { Parent, Root } from 'mdast'; import remarkParse from 'remark-parse'; import { unified } from 'unified'; -import mdxishComponentBlocks, { parseAttributes } from '../../processor/transform/mdxish/mdxish-component-blocks'; +import mdxishComponentBlocks, { parseAttributes } from '../../../processor/transform/mdxish/mdxish-component-blocks'; /** * Helper to parse markdown and apply only the mdxishComponentBlocks plugin. diff --git a/__tests__/transformers/mdxish-heading-slugs.test.ts b/__tests__/transformers/mdxish/mdxish-heading-slugs.test.ts similarity index 99% rename from __tests__/transformers/mdxish-heading-slugs.test.ts rename to __tests__/transformers/mdxish/mdxish-heading-slugs.test.ts index dac86dfd5..99d757aae 100644 --- a/__tests__/transformers/mdxish-heading-slugs.test.ts +++ b/__tests__/transformers/mdxish/mdxish-heading-slugs.test.ts @@ -3,8 +3,8 @@ import type { Heading } from 'mdast'; import { visit } from 'unist-util-visit'; -import { mdxish } from '../../lib'; -import { mdastV6Wrapper } from '../helpers'; +import { mdxish } from '../../../lib'; +import { mdastV6Wrapper } from '../../helpers'; function findAllHeadings(tree: Root): { id: string }[] { const headings: { id: string; }[] = []; diff --git a/__tests__/transformers/normalize-malformed-md-syntax.test.ts b/__tests__/transformers/mdxish/normalize-malformed-md-syntax.test.ts similarity index 99% rename from __tests__/transformers/normalize-malformed-md-syntax.test.ts rename to __tests__/transformers/mdxish/normalize-malformed-md-syntax.test.ts index f84429042..2cdf675e7 100644 --- a/__tests__/transformers/normalize-malformed-md-syntax.test.ts +++ b/__tests__/transformers/mdxish/normalize-malformed-md-syntax.test.ts @@ -17,7 +17,7 @@ import remarkGfm from 'remark-gfm'; import remarkParse from 'remark-parse'; import { removePosition } from 'unist-util-remove-position'; -import normalizeEmphasisAST from '../../processor/transform/mdxish/normalize-malformed-md-syntax'; +import normalizeEmphasisAST from '../../../processor/transform/mdxish/normalize-malformed-md-syntax'; const processor = remark().use(remarkParse).use(normalizeEmphasisAST); const processorWithGfm = remark().use(remarkParse).use(remarkGfm).use(normalizeEmphasisAST); diff --git a/__tests__/transformers/normalize-table-separator.test.ts b/__tests__/transformers/mdxish/normalize-table-separator.test.ts similarity index 99% rename from __tests__/transformers/normalize-table-separator.test.ts rename to __tests__/transformers/mdxish/normalize-table-separator.test.ts index 0feabd9bd..eaf6cfca5 100644 --- a/__tests__/transformers/normalize-table-separator.test.ts +++ b/__tests__/transformers/mdxish/normalize-table-separator.test.ts @@ -1,4 +1,4 @@ -import { normalizeTableSeparator } from '../../processor/transform/mdxish/normalize-table-separator'; +import { normalizeTableSeparator } from '../../../processor/transform/mdxish/normalize-table-separator'; describe('normalize-table-separator', () => { describe('malformed left alignment', () => { diff --git a/__tests__/transformers/preprocess-jsx-expressions.test.ts b/__tests__/transformers/mdxish/preprocess-jsx-expressions.test.ts similarity index 98% rename from __tests__/transformers/preprocess-jsx-expressions.test.ts rename to __tests__/transformers/mdxish/preprocess-jsx-expressions.test.ts index 2803942e7..036a76554 100644 --- a/__tests__/transformers/preprocess-jsx-expressions.test.ts +++ b/__tests__/transformers/mdxish/preprocess-jsx-expressions.test.ts @@ -1,19 +1,6 @@ -import type { Element, Root } from 'hast'; - -import { mdxish } from '../../lib'; -import { JSON_VALUE_MARKER, preprocessJSXExpressions } from '../../processor/transform/mdxish/preprocess-jsx-expressions'; - -// Helper function to find an element by tag name in a hast tree -function findElementByTagName(node: Element | Root, tagName: string): Element | undefined { - if (node.type === 'element' && node.tagName === tagName) return node; - if (!('children' in node)) return undefined; - - return node.children.reduce((found, child) => { - if (found) return found; - if (child.type !== 'element') return undefined; - return findElementByTagName(child, tagName); - }, undefined); -} +import { mdxish } from '../../../lib'; +import { JSON_VALUE_MARKER, preprocessJSXExpressions } from '../../../processor/transform/mdxish/preprocess-jsx-expressions'; +import { findElementByTagName } from '../../helpers'; describe('preprocessJSXExpressions', () => { describe('Step 2: Evaluate attribute expressions', () => { diff --git a/__tests__/transformers/preprocess-redos-attack.test.ts b/__tests__/transformers/mdxish/preprocess-redos-attack.test.ts similarity index 98% rename from __tests__/transformers/preprocess-redos-attack.test.ts rename to __tests__/transformers/mdxish/preprocess-redos-attack.test.ts index 4f1dac204..dd98f7696 100644 --- a/__tests__/transformers/preprocess-redos-attack.test.ts +++ b/__tests__/transformers/mdxish/preprocess-redos-attack.test.ts @@ -1,6 +1,4 @@ -import { describe, it, expect } from 'vitest'; - -import { preprocessJSXExpressions } from '../../processor/transform/mdxish/preprocess-jsx-expressions'; +import { preprocessJSXExpressions } from '../../../processor/transform/mdxish/preprocess-jsx-expressions'; describe('ReDoS Attack Vectors', () => { it('should handle basic attack pattern without hanging', () => { diff --git a/__tests__/transformers/terminate-html-flow-blocks.test.ts b/__tests__/transformers/mdxish/terminate-html-flow-blocks.test.ts similarity index 98% rename from __tests__/transformers/terminate-html-flow-blocks.test.ts rename to __tests__/transformers/mdxish/terminate-html-flow-blocks.test.ts index 3ab100470..803e26b50 100644 --- a/__tests__/transformers/terminate-html-flow-blocks.test.ts +++ b/__tests__/transformers/mdxish/terminate-html-flow-blocks.test.ts @@ -1,4 +1,4 @@ -import { terminateHtmlFlowBlocks } from '../../processor/transform/mdxish/terminate-html-flow-blocks'; +import { terminateHtmlFlowBlocks } from '../../../processor/transform/mdxish/terminate-html-flow-blocks'; describe('terminateHtmlFlowBlocks', () => { describe('when it should insert a blank line', () => {