I found myself in the situation of wanting to analyze a long file of code, of which there are particular parts that are the most relevant. To do this, I could use redundant phrasing to direct a reader to focus on particular lines, or use multiple blocks of code to represent that various chunks of interest. Below are roughly speaking 3 methods that came to mind, from worst to best.
Three examples of ways to discuss long code
One long block
Reading one long block of code, even with highlights to call to attention, requires multiple passes of scanning back and forth to establish context, and to connect the descriptions to code. Presenting code in this way is tiresome to the reader.
Below is an example of a Rehype plugin. From line 233, the visit async function is called from the unist-util-visit package that traverses a HAST (tree) and calls a function for every node that matches the second argument — in this case, nodes with type “element”. The first iteration is to retrieve the languages of all blocks, on line 239. They are aggregated on line 241, and then dynamically loaded on line 266. Then on line 278, a second pass through the tree strips escape characters from the code before parsing it with a cached singleton of the Shiki highlighter object.
Breaking up the blocks allows more in-depth commentary without wasting effort on directing the reader’s attention. It is much easier to focus and grasp the context when the code is limited to the vicinity of interest.
Below is an example of a Rehype plugin. First, the visit async function is called from the unist-util-visit package that traverses a HAST (tree) and calls a function for every node that matches the second argument — in this case, nodes with type “element”. The first iteration is to retrieve the languages, aggregating them in a langsToLoad Set (to eliminate redundancies).
A second pass through the tree strips escape characters from the code before parsing it with Shiki. The elements are checked for validity and the languages are tested to see if loading was successful.
Although content-wise this version is nearly identical to the second one, describing specific lines of code right next to them maintains a single continuous context instead of forcing the reader to endure multiple subconscious context switches. It also allows for the separation of more general statements when context switches are necessary. In my opinion, this makes it much easier to read.
227returnasync(tree)=>{228constlangsToLoad=newSet<string>();229consthighlighter=awaitcachedHighlighter;230if(!highlighter)return;231The 'visit' async function is called from the 'unist-util-visit' package that traverses a HAST (tree) and calls a function for every node that matches the second argument -- in this case, nodes with type 'element'.232//biome-ignorelint/complexity/noExcessiveCognitiveComplexity:<explanation>233visit(tree,'element',(element,_,parent)=>{234if(isInlineCode(element,parent,bypassInlineCode)){235consttextElement=element.children[0];236if(!isText(textElement))return;237constvalue=textElement.value;238if(!value)return;The first iteration retrieves the language from each block, storing unique languages into a Set.239constlang=getInlineCodeLang(value,defaultInlineCodeLang);240if(lang&&lang[0]!=='.'){241langsToLoad.add(lang);242}243}
skip to line 261
261try{262awaitPromise.allSettled(263Array.from(langsToLoad).map((lang)=>{264try{After 'visit' is complete, the retrieved languages are then dynamically loaded using a resolved singleton of a cached Shiki highlighter object.265returnhighlighter.loadLanguage(266langasParameters<typeofhighlighter.loadLanguage>[0],267);268}catch(e){269returnPromise.reject(e);270}
skip to line 277
277//biome-ignorelint/complexity/noExcessiveCognitiveComplexity:<explanation>A second pass through the tree strips escape characters from the code before parsing it with Shiki. The elements are checked for validity and the languages are tested to see if loading was successful.278visit(tree,'element',(element,_,parent)=>{279if(isInlineCode(element,parent,bypassInlineCode)){280consttextElement=element.children[0];281if(!isText(textElement))return;282constvalue=textElement.value;283if(!value)return;
It’s a relief to pass Groundhog Day, I’m sure, but I hope I’ve impressed upon you the importance of a customized syntax highlighting environment. Even if you disagree with my ideas about what is easier to read, the more important point is that each blog author could advance their own ideas, making each blog stand out with a unique style.
What’s more, it’s not that hard to implement!
For the line skips shown above, first recall the overall form of a Transformer object.
For each line that a skip occurs, we want to use a comment with a code to tell it which line to skip to. For example,
typescript
1//Line12//[!cоdeskipto261]3//Line261
causes
typescript
1//Line1
skip to line 261
261//Line261
This means that we should iterate through the lines of the raw code before tokenization to find these codes, then keep a map of line indices to the corresponding numbering.
/src/shiki/transforms/linebreaks.tstypescript
1consttransformer:ShikiTransformer={2preprocess(raw_code:string,meta:CodeOptionsMeta){3constregexp:RegExp=/\[!codeskipto(\d+)\]/;4constlines:string[]=raw_code.split('\n');5letlineNumber=1;We use the meta object to pass objects between functions.6meta['skipMap']=newMap<number,number|null>();7for(leti=0;i<lines.length;i++){8constmatch:Array=lines[i].match(regexp);9if(match){10constskipTo=parseInt(match[1]);11if(!isNaN(skipTo)){Line index is 1-based. The skip itself is doesn't have a line number. Set the next line to the skip-to line.12meta['skipMap'].set(i+1,null);13lineNumber=skipTo;14}15}16else{Post-increment after assigning.17meta['skipMap'].set(i+1,lineNumber++);18}19}20},21//...22};
Now we just need to add several elements to each line that matches our criteria.
/src/shiki/transforms/linebreaks.tstypescript
1import{Element}from'hast';2constcreateElement=(tagName:string):Element=>({3type:'element',tagName,properties:{},children:[]4});5consttransformer:ShikiTransformer={6line(line:Element,index:number){Pass meta object from ShikiTransformer context.7constskipMap:Map<number,number|null>=this.options.meta['skipMap'];8constlineNumber:number|null=skipMap.get(index);9if(lineNumber){Create div to hold line number.10constlineNumberDiv=createElement('div');11lineNumberDiv.properties['data-line-number']='';12lineNumberDiv.children=[{type:'text',value:13lineNumber?lineNumber.toString():''}];Put code tokens under sibling div.14constlineCodeDiv=createElement('div');15lineCodeDiv.properties['data-line-code']='';16lineCodeDiv.children=line.children;17line.children=[lineNumberDiv,lineCodeDiv];18}19else{Lines without a number are skips in this example.20constlineSkip=createElement('div');21lineSkip.properties['data-line-break-skip']='';22consttopSide=createElement('div');23topSide.properties['data-line-break-top']='';24constbottomSide=createElement('div');25bottomSide.properties['data-line-break-bottom']='';26line.properties['data-line-break']='';27line.children=[topSide,lineSkip,bottomSide];28}29returnline;30},31//...32};
Finally, we can give some basic styling that approximates what it looks like above.
css
1.astro-code{2&span[data-line-break]{We want [topSide, lineSkip, bottomSide] to flow vertically.3display:flex;4flex-direction:column;5justify-content:center;6align-items:center;7width:100%;8height:4rem;910&[data-line-break-top],11&[data-line-break-bottom]{The sides should have the same color as the theme background.12@media(prefers-color-scheme:light){13background-color:var(--shiki-light-bg);14}15@media(prefers-color-scheme:dark){16background-color:var(--shiki-dark-bg);17}Keep size fixed.18flex-basis:1rem;19flex-grow:0;20flex-shrink:0;21}2223&[data-line-break-top]{Add some drop shadow from the top. Realistically the colors here need to be styled by theme as well.24box-shadow:0px5px0.75rem0.4rem#99925}2627&[data-line-break-skip]{28background-color:grey;29flex-basis:2rem;30flex-grow:0;31flex-shrink:0;32}33}34}
Obviously, the styles could use some tuning, but the code here took about half an hour to write. I’ve spent more time just trying to get the font right in Wordpress.