1212 * @property {CodeBlock[] } codeBlocks
1313 */
1414
15+ import remarkEscapeComponents , { REPL_LT } from '../../remark-escape-components.js' ;
1516import { buildCompiler } from './build-compiler.js' ;
1617
1718export { buildCompiler } from './build-compiler.js' ;
@@ -22,36 +23,67 @@ export { buildCompiler } from './build-compiler.js';
2223 * @returns {Promise<ParseResult> }
2324 */
2425export async function parseMarkdown ( input , options ) {
25- const markdownCompiler = options ?. compiler ?? buildCompiler ( options ) ;
26+ const markdownCompiler =
27+ options ?. compiler ??
28+ buildCompiler ( {
29+ ...options ,
30+ remarkPlugins : [ ...( options ?. remarkPlugins || [ ] ) , remarkEscapeComponents ] ,
31+ } ) ;
2632 const processed = await markdownCompiler . process ( input ) ;
2733 const liveCode = /** @type {CodeBlock[] } */ ( processed . data . liveCode || [ ] ) ;
2834 // @ts -ignore - processed is typed as unknown due to unified processor complexity
2935 let templateOnly = processed . toString ( ) ;
3036
31- // Unescape PascalCase components that had only the opening < HTML-entity escaped
32- // BUT only outside of <pre><code> blocks where escaping should be preserved
33- // (inline <code> tags should have components unescaped)
34- // Split by <pre><code>...</code></pre> to exclude only code blocks
35- const parts = templateOnly . split ( / ( < p r e [ ^ > ] * > .* ?< \/ p r e > ) / is) ;
37+ // 1. Convert the placeholder written by the remark plugin to <
38+ // This placeholder survives the entire unified pipeline without being
39+ // entity-encoded, so no double-escaping can occur.
40+ if ( REPL_LT ) {
41+ templateOnly = templateOnly . replaceAll ( REPL_LT , '<' ) ;
42+ }
43+
44+ // 2. The pipeline may HTML-escape `<` for PascalCase component invocations
45+ // that appear in regular markdown (outside code/backticks). Undo that so
46+ // Glimmer can still invoke them. We only unescape outside <code> elements
47+ // (and outside <pre> blocks) to preserve escaping in code.
48+ templateOnly = unescapeComponentsOutsideCode ( templateOnly ) ;
49+
50+ return { text : templateOnly , codeBlocks : liveCode } ;
51+ }
52+
53+ /**
54+ * Undo HTML-escaping of PascalCase component tags that appear outside
55+ * `<code>` and `<pre>` blocks so Glimmer can invoke them.
56+ *
57+ * @param {string } html
58+ * @returns {string }
59+ */
60+ function unescapeComponentsOutsideCode ( html ) {
61+ // Split by <pre>…</pre> blocks first – never touch code fences.
62+ const parts = html . split ( / ( < p r e [ \s \S ] * ?< \/ p r e > ) / gi) ;
3663
3764 for ( let i = 0 ; i < parts . length ; i ++ ) {
38- const part = parts [ i ] ;
39-
40- // Only process parts that are NOT pre blocks (odd indices are pre blocks)
41- if ( i % 2 === 0 && part ) {
42- // Pattern: <ComponentName ... / > (only < is escaped as <)
43- parts [ i ] = part . replace ( / & # x 3 C ; ( [ A - Z ] [ a - z A - Z 0 - 9 ] * \s [ ^ < ] * ?) > / g, ( match , content ) => {
44- // Only unescape if it contains @ (attribute) indicating a component
45- if ( content . includes ( '@' ) ) {
46- return `<${ content } >` ;
65+ // Only touch content outside <pre>
66+ if ( i % 2 === 0 ) {
67+ // Split by <code>…</code> so we skip inline code too
68+ const part = parts [ i ] ?? '' ;
69+ const codeParts = part . split ( / ( < c o d e [ ^ > ] * > [ \s \S ] * ?< \/ c o d e > ) / gi) ;
70+
71+ for ( let j = 0 ; j < codeParts . length ; j ++ ) {
72+ const segment = codeParts [ j ] ;
73+
74+ // Even indices are outside <code> – unescape PascalCase there
75+ if ( j % 2 === 0 && segment ) {
76+ codeParts [ j ] = segment
77+ . replace ( / & # x 3 C ; ( [ A - Z ] [ a - z A - Z 0 - 9 ] * \s [ ^ < ] * ?) > / g, ( _m , content ) =>
78+ content . includes ( '@' ) ? `<${ content } >` : _m
79+ )
80+ . replace ( / & # x 3 C ; \/ ( [ A - Z ] [ a - z A - Z 0 - 9 ] * ) > / g, '</$1>' ) ;
4781 }
82+ }
4883
49- return match ;
50- } ) ;
84+ parts [ i ] = codeParts . join ( '' ) ;
5185 }
5286 }
5387
54- templateOnly = parts . join ( '' ) ;
55-
56- return { text : templateOnly , codeBlocks : liveCode } ;
88+ return parts . join ( '' ) ;
5789}
0 commit comments