• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gmsgowtham / react-native-marked / 20273357353

16 Dec 2025 03:29PM UTC coverage: 90.73% (-0.5%) from 91.218%
20273357353

Pull #948

github

web-flow
Merge ff64d84e8 into 0d1fc1f8b
Pull Request #948: feat(markdown): add react component embedding support

155 of 186 branches covered (83.33%)

Branch coverage included in aggregate %.

91 of 94 new or added lines in 3 files covered. (96.81%)

305 of 321 relevant lines covered (95.02%)

24.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.14
/src/lib/ReactComponentTokenizer.ts
1
import type { Tokens } from "marked";
2
import { Tokenizer } from "marked";
3

4
const SELF_CLOSING_REGEX = /^<([A-Z][a-zA-Z0-9]*)([^>]*?)\s*\/>/;
2✔
5
const OPENING_TAG_REGEX = /^<([A-Z][a-zA-Z0-9]*)([^>]*)>/;
2✔
6
const PROP_REGEX = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}))?/g;
2✔
7

8
export type ReactComponentProps = Record<string, string | boolean | number>;
9

10
export interface ReactComponentToken extends Tokens.HTML {
11
        componentName: string;
12
        componentProps: ReactComponentProps;
13
        componentChildren: string;
14
}
15

16
function parseProps(propsString: string): ReactComponentProps {
17
        const props: ReactComponentProps = {};
26✔
18
        if (!propsString) return props;
26✔
19

20
        let match: RegExpExecArray | null;
21

22
        match = PROP_REGEX.exec(propsString);
12✔
23
        while (match !== null) {
12✔
24
                const [, propName, doubleQuoted, singleQuoted, braced] = match;
16✔
25
                if (propName) {
16!
26
                        if (doubleQuoted !== undefined) {
16✔
27
                                props[propName] = doubleQuoted;
7✔
28
                        } else if (singleQuoted !== undefined) {
9✔
29
                                props[propName] = singleQuoted;
2✔
30
                        } else if (braced !== undefined) {
7✔
31
                                const trimmed = braced.trim();
5✔
32
                                if (trimmed === "true") {
5✔
33
                                        props[propName] = true;
2✔
34
                                } else if (trimmed === "false") {
3!
NEW
35
                                        props[propName] = false;
×
36
                                } else if (!Number.isNaN(Number(trimmed))) {
3!
37
                                        props[propName] = Number(trimmed);
3✔
38
                                } else {
NEW
39
                                        props[propName] = trimmed;
×
40
                                }
41
                        } else {
42
                                props[propName] = true;
2✔
43
                        }
44
                }
45
                match = PROP_REGEX.exec(propsString);
16✔
46
        }
47

48
        PROP_REGEX.lastIndex = 0;
12✔
49
        return props;
12✔
50
}
51

52
function findClosingTag(
53
        src: string,
54
        componentName: string,
55
        startIndex: number,
56
): { endIndex: number; children: string } | null {
57
        const openingTag = new RegExp(`<${componentName}(?:\\s[^>]*)?>`, "g");
7✔
58
        const closingTag = `</${componentName}>`;
7✔
59

60
        let depth = 1;
7✔
61
        let currentIndex = startIndex;
7✔
62

63
        while (depth > 0 && currentIndex < src.length) {
7✔
64
                const closingIndex = src.indexOf(closingTag, currentIndex);
8✔
65
                if (closingIndex === -1) return null;
8!
66

67
                openingTag.lastIndex = currentIndex;
8✔
68
                let nestedMatch: RegExpExecArray | null;
69
                nestedMatch = openingTag.exec(src);
8✔
70
                while (nestedMatch !== null && nestedMatch.index < closingIndex) {
8✔
71
                        depth++;
1✔
72
                        nestedMatch = openingTag.exec(src);
1✔
73
                }
74

75
                depth--;
8✔
76
                if (depth === 0) {
8✔
77
                        return {
7✔
78
                                endIndex: closingIndex + closingTag.length,
79
                                children: src.slice(startIndex, closingIndex),
80
                        };
81
                }
82
                currentIndex = closingIndex + closingTag.length;
1✔
83
        }
84

NEW
85
        return null;
×
86
}
87

88
export class ReactComponentTokenizer extends Tokenizer {
89
        override html(src: string): Tokens.HTML | undefined {
90
                const selfClosingMatch = SELF_CLOSING_REGEX.exec(src);
29✔
91
                if (selfClosingMatch) {
29✔
92
                        const [raw, componentName, propsString] = selfClosingMatch;
19✔
93
                        return this.createReactComponentToken(
19✔
94
                                raw,
95
                                componentName ?? "",
19!
96
                                propsString ?? "",
19!
97
                                "",
98
                        );
99
                }
100

101
                const openingMatch = OPENING_TAG_REGEX.exec(src);
10✔
102
                if (openingMatch) {
10✔
103
                        const [openingTag, componentName, propsString] = openingMatch;
7✔
104
                        const result = findClosingTag(
7✔
105
                                src,
106
                                componentName ?? "",
7!
107
                                openingTag?.length ?? 0,
7!
108
                        );
109

110
                        if (result) {
7!
111
                                const raw = src.slice(0, result.endIndex);
7✔
112
                                return this.createReactComponentToken(
7✔
113
                                        raw,
114
                                        componentName ?? "",
7!
115
                                        propsString ?? "",
7!
116
                                        result.children,
117
                                );
118
                        }
119
                }
120

121
                return super.html(src);
3✔
122
        }
123

124
        private createReactComponentToken(
125
                raw: string,
126
                componentName: string,
127
                propsString: string,
128
                children: string,
129
        ): ReactComponentToken {
130
                return {
26✔
131
                        type: "html",
132
                        raw,
133
                        text: raw,
134
                        block: true,
135
                        pre: false,
136
                        componentName,
137
                        componentProps: parseProps(propsString),
138
                        componentChildren: children.trim(),
139
                };
140
        }
141
}
142

143
export function isReactComponentToken(
144
        token: Tokens.HTML,
145
): token is ReactComponentToken {
146
        return "componentName" in token && typeof token.componentName === "string";
16✔
147
}
148

149
export default ReactComponentTokenizer;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc