Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: matching jsx component logic in string fragments #1861

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/normal/.dumi/theme/builtins/Identity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

export interface IdentityProps {
[key: string]: React.ReactNode;
}

function Identity(props: React.PropsWithChildren<IdentityProps>) {
const { children, ...restProps } = props;
return (
<ul>
{Object.entries(restProps).map(([key, value]) => (
<li key={key}>{value}</li>
))}
{children && <li>{children}</li>}
</ul>
);
}

export default Identity;
50 changes: 50 additions & 0 deletions examples/normal/docs/hello/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,53 @@ group: 测试分组
## This is hello/index.md

约定式导航、分组测试

## Issue 1836

> debug https://github.com/umijs/dumi/issues/1836

### case 01

<Identity>Hello Dumi!</Identity>

```tsx | pure
<Identity>Hello Dumi!</Identity>
```

### case 02

<Identity name="Awesome"></Identity>

```tsx | pure
<Identity name="Awesome"></Identity>
```

### case 03

<Identity name="Required<Props>"></Identity>

```tsx | pure
<Identity name="Required<Props>"></Identity>
```

### case 04

<Identity>
Hello Dumi!
<Identity name="Awesome"></Identity>
<Identity name="Required<Props>"></Identity>
<Identity name="Required<Props>">
<Identity name="Awesome">Awesome Children</Identity>
</Identity>
</Identity>

```tsx | pure
<Identity>
Hello Dumi!
<Identity name="Awesome"></Identity>
<Identity name="Required<Props>"></Identity>
<Identity name="Required<Props>">
<Identity name="Awesome">Awesome Children</Identity>
</Identity>
</Identity>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`rehypeRaw > addCustomPropsToMatchingJSX > should match nested jsx components 1`] = `
"<Foo $tag-name=\\"Foo\\" props=\\"123\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"></Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\">bar</Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"><Foo $tag-name=\\"Foo\\" props=\\"123\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"></Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\">bar</Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"><Foo $tag-name=\\"Foo\\" props=\\"123\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"></Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\">bar</Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\">bar</Foo></Foo></Foo>"
`;
69 changes: 69 additions & 0 deletions src/loaders/markdown/transformer/rehypeRaw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { __private__ } from './rehypeRaw';

const { addCustomPropsToMatchingJSX } = __private__;

describe('rehypeRaw', () => {
describe('addCustomPropsToMatchingJSX', () => {
it('should return the original string when there are no jsx fragments to match', () => {
const input = 'foo bar';
expect(addCustomPropsToMatchingJSX(input)).toBe(input);
});

it('should match jsx fragments without children', () => {
const input = '<Foo />';
expect(addCustomPropsToMatchingJSX(input)).toMatchInlineSnapshot(
'"<Foo $tag-name=\\"Foo\\" />"',
);
});

it('should match jsx fragments that contain children', () => {
const input = '<Foo>bar</Foo>';
expect(addCustomPropsToMatchingJSX(input)).toMatchInlineSnapshot(
'"<Foo $tag-name=\\"Foo\\" >bar</Foo>"',
);
});

it('should match multiple jsx fragments', () => {
const input = `
<Foo>bar</Foo>
<Bar>foo</Bar>
<Footer />
`.trim();
expect(addCustomPropsToMatchingJSX(input)).toMatchInlineSnapshot(`
"<Foo $tag-name=\\"Foo\\" >bar</Foo>
<Bar $tag-name=\\"Bar\\" >foo</Bar>
<Footer $tag-name=\\"Footer\\" />"
`);
});

it('should match jsx fragments containing props', () => {
const input = `
<Foo props="123" />
<Foo props="ReutersType<Props>" />
<Foo props="ReutersType<Props>"></Foo>
<Foo props="ReutersType<Props>">bar</Foo>
`.trim();
expect(addCustomPropsToMatchingJSX(input)).toMatchInlineSnapshot(`
"<Foo $tag-name=\\"Foo\\" props=\\"123\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\" />
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\"></Foo>
<Foo $tag-name=\\"Foo\\" props=\\"ReutersType<Props>\\">bar</Foo>"
`);
});

it('should match nested jsx components', () => {
const createInput = (childrenStr: string) =>
`
<Foo props="123" />
<Foo props="ReutersType<Props>" />
<Foo props="ReutersType<Props>"></Foo>
<Foo props="ReutersType<Props>">bar</Foo>
<Foo props="ReutersType<Props>">${childrenStr}</Foo>
`.trim();

// nested
const input = createInput(createInput(createInput('bar')));
expect(addCustomPropsToMatchingJSX(input)).toMatchSnapshot();
});
});
});
32 changes: 14 additions & 18 deletions src/loaders/markdown/transformer/rehypeRaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import type { IMdTransformerOptions } from '.';

let raw: typeof import('hast-util-raw').raw;
let visit: typeof import('unist-util-visit').visit;
const COMPONENT_NAME_REGEX = /<[A-Z][a-zA-Z\d]*/g;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个改动删掉的逻辑会导致驼峰 props 的丢失,比如 <Foo relativePath="xxx"></Foo> 会被转成 <Foo relative-path="xxx"></Foo>

另外看了下关键改动点,应该就是正则的改动从匹配 <Foo 变成匹配 <Foo xxx>,虽然能解 issue 提到的 case 但仍然存在字符串属性值里的 JSX 被误伤的可能性,比如 <Foo str="<Bar xxx></Bar>" 应该还是会误伤,只要还是用正则而不是 AST 误伤就不可避免

建议还是按原逻辑替换,在最后一步遍历 properties 恢复 key 名的时候,多加一步恢复 value 的逻辑,整体流程是先误伤、再恢复,因为到恢复这一步的时候,hast-util-raw 已经通过 AST 解析把真正的标签处理过了,剩下的都是在 properties 里的字符串标签,正好是之前误伤的内容

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体流程是先误伤、再恢复,因为到恢复这一步的时候,hast-util-raw 已经通过 AST 解析把真正的标签处理过了,

误伤后给 raw 处理得到的 properties 是有问题的。回复处理有点困难

我这边的解决思路还是用这个 pr, 然后继续跟进你说的 驼峰 props 丢失问题。先挂一下问题,对我来说有些挑战

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

试了下 properties 的确已经乱了,我换了种思路解决,有空可以帮忙 review 下:#1872

const COMPONENT_PROP_REGEX = /\s[a-z][a-z\d]*[A-Z]+[a-zA-Z\d]*(=|\s|>)/g;
const JSX_PATTERN = /<([A-Z][A-Za-z0-9]*)\s*([^>]*)\/?>/g;
const COMPONENT_STUB_ATTR = '$tag-name';
const PROP_STUB_ATTR = '-$u';
const PROP_STUB_ATTR_REGEX = new RegExp(
Expand All @@ -21,27 +20,17 @@ const CODE_META_STUB_ATTR = '$code-meta';
({ raw } = await import('hast-util-raw'));
})();

const addCustomPropsToMatchingJSX = (jsxStr: string) => {
return jsxStr.replace(JSX_PATTERN, `<$1 ${COMPONENT_STUB_ATTR}="$1" $2>`);
};

type IRehypeRawOptions = Pick<IMdTransformerOptions, 'fileAbsPath'>;

export default function rehypeRaw(opts: IRehypeRawOptions): Transformer<Root> {
return (tree, vFile) => {
visit<Root>(tree, (node) => {
if (node.type === 'raw' && COMPONENT_NAME_REGEX.test(node.value)) {
// mark tagName for all custom react component
// because the parse5 within hast-util-raw will lowercase all tag names
node.value = node.value.replace(COMPONENT_NAME_REGEX, (str) => {
const tagName = str.slice(1);

return `${str} ${COMPONENT_STUB_ATTR}="${tagName}"`;
});
// mark all camelCase props for all custom react component
// because the parse5 within hast-util-raw will lowercase all attr names
node.value = node.value.replace(COMPONENT_PROP_REGEX, (str) => {
return str.replace(
/[A-Z]/g,
(s) => `${PROP_STUB_ATTR}${s.toLowerCase()}`,
);
});
if (node.type === 'raw' && JSX_PATTERN.exec(node.value)) {
node.value = addCustomPropsToMatchingJSX(node.value);
} else if (node.type === 'element' && node.data?.meta) {
// save code meta to properties to avoid lost
// ref: https://github.com/syntax-tree/hast-util-raw/issues/13#issuecomment-912451531
Expand Down Expand Up @@ -86,3 +75,10 @@ File: ${opts.fileAbsPath}`);
return newTree;
};
}

/**
* @internal **仅用于测试用例使用。**
*/
export const __private__ = {
addCustomPropsToMatchingJSX,
};