@@ -182,6 +182,80 @@ export function highlightCode() {
182182 blocks . forEach ( ( block ) => {
183183 Prism . highlightElement ( block ) ;
184184 } ) ;
185+
186+ // After Prism highlighting, add per-line data-source-line spans inside code blocks
187+ _annotateCodeBlockLines ( ) ;
188+ }
189+
190+ /**
191+ * Wrap each line in highlighted code blocks with a span that has data-source-line,
192+ * enabling per-line cursor sync for code blocks.
193+ * Must run AFTER Prism highlighting since Prism replaces innerHTML.
194+ */
195+ function _annotateCodeBlockLines ( ) {
196+ const pres = document . querySelectorAll ( "#viewer-content pre[data-source-line]" ) ;
197+ pres . forEach ( ( pre ) => {
198+ const code = pre . querySelector ( "code" ) ;
199+ if ( ! code ) return ;
200+ const preSourceLine = parseInt ( pre . getAttribute ( "data-source-line" ) , 10 ) ;
201+ if ( isNaN ( preSourceLine ) ) return ;
202+ // Code content starts after the ``` line
203+ const codeStartLine = preSourceLine + 1 ;
204+
205+ // Split the code's child nodes by newlines and wrap each line
206+ const fragment = document . createDocumentFragment ( ) ;
207+ let currentLine = document . createElement ( "span" ) ;
208+ currentLine . setAttribute ( "data-source-line" , String ( codeStartLine ) ) ;
209+ let lineIdx = 0 ;
210+
211+ function processNode ( node ) {
212+ if ( node . nodeType === Node . TEXT_NODE ) {
213+ const text = node . textContent ;
214+ const parts = text . split ( "\n" ) ;
215+ for ( let i = 0 ; i < parts . length ; i ++ ) {
216+ if ( i > 0 ) {
217+ // Close current line span, start new one with the newline inside it
218+ fragment . appendChild ( currentLine ) ;
219+ lineIdx ++ ;
220+ currentLine = document . createElement ( "span" ) ;
221+ currentLine . setAttribute ( "data-source-line" , String ( codeStartLine + lineIdx ) ) ;
222+ currentLine . appendChild ( document . createTextNode ( "\n" ) ) ;
223+ }
224+ if ( parts [ i ] ) {
225+ currentLine . appendChild ( document . createTextNode ( parts [ i ] ) ) ;
226+ }
227+ }
228+ } else if ( node . nodeType === Node . ELEMENT_NODE ) {
229+ // Check if this element contains newlines
230+ const text = node . textContent ;
231+ if ( ! text . includes ( "\n" ) ) {
232+ // No newlines — append the whole element to current line
233+ currentLine . appendChild ( node . cloneNode ( true ) ) ;
234+ } else {
235+ // Element spans multiple lines — process children
236+ for ( const child of Array . from ( node . childNodes ) ) {
237+ processNode ( child ) ;
238+ }
239+ }
240+ }
241+ }
242+
243+ const children = Array . from ( code . childNodes ) ;
244+ for ( const child of children ) {
245+ processNode ( child ) ;
246+ }
247+ // Append the last line
248+ if ( currentLine . childNodes . length > 0 ) {
249+ fragment . appendChild ( currentLine ) ;
250+ }
251+
252+ code . innerHTML = "" ;
253+ code . appendChild ( fragment ) ;
254+
255+ // Remove data-source-line from <pre> so clicking empty areas inside the
256+ // code block doesn't fall through to the block's start line
257+ pre . removeAttribute ( "data-source-line" ) ;
258+ } ) ;
185259}
186260
187261function addCopyButtons ( ) {
0 commit comments