@@ -116,6 +116,103 @@ function walkImages(root, fn) {
116116 if ( root . children ) walkImages ( root . children , fn ) ;
117117}
118118
119+ // Apply `fn` to every node in the tree (mutating in place). Unlike
120+ // walkImages this visits every node type, not just images.
121+ function walkAll ( node , fn ) {
122+ if ( ! node ) return ;
123+ if ( Array . isArray ( node ) ) {
124+ node . forEach ( ( c ) => walkAll ( c , fn ) ) ;
125+ return ;
126+ }
127+ fn ( node ) ;
128+ if ( Array . isArray ( node . children ) ) walkAll ( node . children , fn ) ;
129+ }
130+
131+ // makeindex treats `! @ " |` as control characters; literal occurrences
132+ // inside an \index{...} argument must be prefixed with `"` (the default
133+ // quote char) so makeindex doesn't try to split them into sub-entries
134+ // or alternate-rendering markers.
135+ function quoteForMakeindex ( s ) {
136+ return s . replace ( / ( [ " @ ! | ] ) / g, '"$1' ) ;
137+ }
138+
139+ // Convert a single index entry string so that runs of `code` render as
140+ // \texttt{...} in the printed index, while still sorting on the plain
141+ // text. We rewrite the string into makeindex's `sort@display` form,
142+ // which tells makeindex to alphabetize on the part before the `@` but
143+ // typeset the part after it. Without this, leading backticks would
144+ // sort the entry under "Symbols" and render as curly quotes.
145+ //
146+ // `⚡` (and the variation-selector form `⚡️`) is similarly recoded so
147+ // it sorts under "lightning bolt" and is typeset via \snaplightning,
148+ // which is defined in the preamble (the body font has no glyph for
149+ // ⚡, so passing it through verbatim renders as a missing-glyph box).
150+
151+ function rewriteIndexEntry ( value ) {
152+ if ( typeof value !== 'string' ) return value ;
153+ // Trailing backslashes on index entries (sometimes left over from
154+ // markdown line-continuation syntax in the source) would escape the
155+ // closing brace of \index{...} when written to LaTeX. Strip them
156+ // unconditionally — there's never a legitimate use for them inside
157+ // an index term.
158+ value = value . replace ( / \\ + \s * $ / , '' ) . trim ( ) ;
159+ const hasCode = value . includes ( '`' ) ;
160+ const hasBolt = / [ ⚡ ] / . test ( value ) ;
161+ if ( ! hasCode && ! hasBolt ) return value ;
162+ // Display: `set` -> \texttt{set}; ⚡ (with optional VS-16) -> \snaplightning{}.
163+ // # / % / & are parameter / comment / tab-alignment characters in LaTeX
164+ // and would break the .ind file makeindex emits if left bare inside the
165+ // \texttt{...} group. _ is already typically escaped by myst upstream
166+ // but we double-escape defensively.
167+ const display = quoteForMakeindex (
168+ value
169+ . replace ( / ` ( [ ^ ` ] + ) ` / g, ( _m , code ) =>
170+ `\\texttt{${ code . replace ( / (?< ! \\ ) ( [ # % & _ ] ) / g, '\\$1' ) } }` ,
171+ )
172+ . replace ( / ⚡ ️ ? / g, '\\snaplightning{}' ) ,
173+ ) ;
174+ // Sort key: drop the formatting markers entirely, collapse the
175+ // resulting whitespace, and substitute "lightning bolt" for ⚡ so
176+ // the entry alphabetizes near "L" rather than the symbol section.
177+ const sort = quoteForMakeindex (
178+ value
179+ . replace ( / ` / g, '' )
180+ . replace ( / ⚡ ️ ? \s * / g, 'lightning bolt ' )
181+ . replace ( / \s + / g, ' ' )
182+ . trim ( ) ,
183+ ) ;
184+ return `${ sort } @${ display } ` ;
185+ }
186+
187+ // Replace ⚡ (with optional VS-16) inside a text node with raw TeX
188+ // pointing at \snaplightning. Returns either the original node, a
189+ // single replacement, or an array of nodes when the bolt appears
190+ // in the middle of a longer string.
191+ function expandLightningInTextNode ( node ) {
192+ if ( node . type !== 'text' || typeof node . value !== 'string' ) return null ;
193+ if ( ! / [ ⚡ ] / . test ( node . value ) ) return null ;
194+ const parts = node . value . split ( / ⚡ ️ ? / ) ;
195+ const out = [ ] ;
196+ parts . forEach ( ( part , idx ) => {
197+ if ( part ) out . push ( { type : 'text' , value : part } ) ;
198+ if ( idx < parts . length - 1 ) {
199+ out . push ( { type : 'raw' , lang : 'tex' , tex : '\\snaplightning{}' } ) ;
200+ }
201+ } ) ;
202+ return out ;
203+ }
204+
205+ function rewriteLightningInChildren ( parent ) {
206+ if ( ! Array . isArray ( parent . children ) ) return ;
207+ for ( let i = 0 ; i < parent . children . length ; i ++ ) {
208+ const replacement = expandLightningInTextNode ( parent . children [ i ] ) ;
209+ if ( replacement ) {
210+ parent . children . splice ( i , 1 , ...replacement ) ;
211+ i += replacement . length - 1 ;
212+ }
213+ }
214+ }
215+
119216const latexShimsTransform = {
120217 name : 'latex-shims' ,
121218 stage : 'document' ,
@@ -184,7 +281,29 @@ const latexShimsTransform = {
184281 gridNode . children = newChildren ;
185282 } ) ;
186283
187- // 3. Image sizing.
284+ // 3. Index entries: rewrite `code` and ⚡ inside index entries into
285+ // a makeindex sort@display string so the printed index uses
286+ // \texttt{...} / \snaplightning instead of literal backticks
287+ // or missing-glyph boxes (and so the entries sort properly).
288+ walkAll ( tree , ( node ) => {
289+ if ( ! Array . isArray ( node . indexEntries ) ) return ;
290+ node . indexEntries . forEach ( ( ie ) => {
291+ if ( typeof ie ?. entry === 'string' ) {
292+ ie . entry = rewriteIndexEntry ( ie . entry ) ;
293+ }
294+ if ( ie ?. subEntry && typeof ie . subEntry . value === 'string' ) {
295+ ie . subEntry . value = rewriteIndexEntry ( ie . subEntry . value ) ;
296+ }
297+ } ) ;
298+ } ) ;
299+
300+ // 4. Lightning bolt in body text. Source Serif Pro has no glyph
301+ // for ⚡, so we splice in a \snaplightning{} raw-TeX node
302+ // wherever the emoji appears. Index-entry strings were already
303+ // handled above.
304+ walkAll ( tree , ( node ) => rewriteLightningInChildren ( node ) ) ;
305+
306+ // 5. Image sizing.
188307 // Inline images get a sentinel width that the custom \includegraphics
189308 // redefinition in the preamble decodes back into a height-based,
190309 // raisebox'd \includegraphics. Block images that have no explicit
@@ -207,6 +326,6 @@ const latexShimsTransform = {
207326} ;
208327
209328export default {
210- name : 'LaTeX shims (kbd, grid, image)' ,
329+ name : 'LaTeX shims (kbd, grid, image, index, lightning )' ,
211330 transforms : [ latexShimsTransform ] ,
212331} ;
0 commit comments