1+ const targetSelector = '.content-container' ;
2+
3+ let htmlPolicy = { createHTML : ( string ) => string } ;
4+ if ( window . trustedTypes && window . trustedTypes . createPolicy ) {
5+ try {
6+ htmlPolicy = window . trustedTypes . createPolicy ( 're-build-engine' , {
7+ createHTML : ( string ) => string ,
8+ } ) ;
9+ } catch ( e ) { console . warn ( e ) ; }
10+ }
11+
12+ function serializeToRawText ( node ) {
13+ // 1. 텍스트 노드면 값 그대로 반환
14+ if ( node . nodeType === Node . TEXT_NODE ) {
15+ return node . nodeValue ;
16+ }
17+
18+ // 2. 요소 노드 처리
19+ if ( node . nodeType === Node . ELEMENT_NODE ) {
20+ const tagName = node . tagName . toUpperCase ( ) ;
21+
22+ // <i> 태그 -> *내용*
23+ if ( tagName === 'I' || tagName === 'EM' ) {
24+ return `*${ Array . from ( node . childNodes ) . map ( serializeToRawText ) . join ( '' ) } *` ;
25+ }
26+
27+ // <b>, <strong> 태그 -> **내용**
28+ if ( tagName === 'B' || tagName === 'STRONG' ) {
29+ return `**${ Array . from ( node . childNodes ) . map ( serializeToRawText ) . join ( '' ) } **` ;
30+ }
31+
32+ // <br> 태그 -> 줄바꿈
33+ if ( tagName === 'BR' ) {
34+ return '\n' ;
35+ }
36+
37+ // <span class="math-inline"> -> $data-math값$
38+ if ( node . classList . contains ( 'math-inline' ) ) {
39+ const mathData = node . getAttribute ( 'data-math' ) ;
40+ if ( mathData ) {
41+ // data-math 값이 있으면 $...$ 로 감싸서 반환
42+ return `$${ mathData } $` ;
43+ }
44+ // data-math가 없으면 내부 텍스트라도 건짐
45+ }
46+
47+ // 그 외 일반 태그(span 등)는 껍데기 벗기고 내용물만 재귀적으로 수집
48+ return Array . from ( node . childNodes ) . map ( serializeToRawText ) . join ( '' ) ;
49+ }
50+
51+ return '' ;
52+ }
53+
54+ function parseAndRender ( rawText ) {
55+ let processedHtml = rawText
56+ . replace ( / & / g, "&" )
57+ . replace ( / < / g, "<" )
58+ . replace ( / > / g, ">" ) ;
59+
60+ // 1. 인라인 코드 보호 (`...`)
61+ // 코드 블록 안의 *, $ 가 파싱되는 것을 방지
62+ const codeBlocks = [ ] ;
63+ processedHtml = processedHtml . replace ( / ( ` + ) ( .* ?) \1/ g, ( match , tick , content ) => {
64+ codeBlocks . push ( `<code style="background:#eee; padding:2px 4px; border-radius:3px; font-family:monospace;">${ content } </code>` ) ;
65+ return `__CODE_BLOCK_${ codeBlocks . length - 1 } __` ;
66+ } ) ;
67+
68+ // 2. LaTeX 수식 파싱 ($...$)
69+ if ( typeof katex !== 'undefined' ) {
70+ processedHtml = processedHtml . replace ( / (?< ! \\ ) \$ ( [ ^ $ ] + ?) \$ / g, ( match , latex ) => {
71+ try {
72+ // HTML entity로 변환된 문자들을 다시 되돌려야 KaTeX가 인식함 (< -> <)
73+ const cleanLatex = latex
74+ . replace ( / & l t ; / g, "<" )
75+ . replace ( / & g t ; / g, ">" )
76+ . replace ( / & a m p ; / g, "&" ) ;
77+
78+ const rendered = katex . renderToString ( cleanLatex , {
79+ throwOnError : false ,
80+ output : 'html' ,
81+ displayMode : false
82+ } ) ;
83+ return `<span class="math-inline-wrapper">${ rendered } </span>` ;
84+ } catch ( e ) {
85+ return match ;
86+ }
87+ } ) ;
88+ }
89+
90+ // 3. Markdown 서식 파싱
91+ // **Bold**
92+ processedHtml = processedHtml . replace ( / \* \* ( .* ?) \* \* / g, '<b>$1</b>' ) ;
93+ // *Italic* (Step 1에서 i 태그를 *로 바꿨으므로 여기서 다시 i 태그로 복구됨)
94+ processedHtml = processedHtml . replace ( / (?< ! \* ) \* (? ! \* ) ( .* ?) \* / g, '<i>$1</i>' ) ;
95+
96+ // 4. 코드 블록 복구
97+ processedHtml = processedHtml . replace ( / _ _ C O D E _ B L O C K _ ( \d + ) _ _ / g, ( match , index ) => {
98+ return codeBlocks [ index ] ;
99+ } ) ;
100+
101+ // 5. 줄바꿈 처리
102+ processedHtml = processedHtml . replace ( / \n / g, '<br>' ) ;
103+
104+ return processedHtml ;
105+ }
106+
107+ function reRenderContent ( ) {
108+ const container = document . querySelector ( targetSelector ) ;
109+ if ( ! container ) return ;
110+
111+ // 아직 건드리지 않은 p 태그들 수집
112+ const paragraphs = container . querySelectorAll ( 'p:not([data-rerendered="true"])' ) ;
113+
114+ paragraphs . forEach ( p => {
115+ // 기존 DOM 해체
116+ const rawText = serializeToRawText ( p ) ;
117+
118+ // 바꿀거 없으면 스킵
119+ if ( ! rawText . trim ( ) || ! rawText . match ( / \* | \$ | ` / ) ) {
120+ p . setAttribute ( 'data-rerendered' , 'true' ) ;
121+ return ;
122+ }
123+
124+ // 텍스트 재렌더링
125+ const newHtml = parseAndRender ( rawText ) ;
126+
127+ // DOM 덮어씌우기
128+ if ( p . innerHTML !== newHtml ) {
129+ p . innerHTML = htmlPolicy . createHTML ( newHtml ) ;
130+ }
131+
132+ // 처리 완료 표시
133+ p . setAttribute ( 'data-rerendered' , 'true' ) ;
134+ } ) ;
135+ }
136+
137+ const observer = new MutationObserver ( ( mutations ) => {
138+ const needsUpdate = mutations . some ( m =>
139+ m . addedNodes . length > 0 ||
140+ ( m . type === 'childList' && m . target . tagName === 'P' )
141+ ) ;
142+ if ( needsUpdate ) reRenderContent ( ) ;
143+ } ) ;
144+
145+ observer . observe ( document . body , {
146+ childList : true ,
147+ subtree : true ,
148+ characterData : true
149+ } ) ;
150+
151+ reRenderContent ( ) ;
0 commit comments