Thoughts .toString()

Code Block Reference


Code Block

The code block is a number of transformers built on top of Shiki. I needed some functionality not supported out of the box in either Shiki or Rehype Pretty (and Rehype has a lot of functionality I don’t want). So I made my own. Some important considerations are that

  • Long lines should wrap into multiple lines at a reasonable indent level. They should not interfere with the line numbering, at any browser width.
  • There should be flexibility in highlighting and annotations that make discussing the code easier.
  • The ability to cite sources is a must.
  • It must be easy to start the code with any indentation level without having to make the actual code ugly due to mismatched indentations. Luckily, using Markdown processing makes this much easier. Picture this:
html
1<body>
2    <div class="content">
3        <h1>Sample Code</h1>
4        <pre>
5The code must start all the way back here to get rid of unnecessary
6whitespace, despite the html being two tabs in at this point. This
7violates my aesthetic sense.
8        </pre>
9    </div>
10</body>
  • Meta options should be easy to write and read.
  • Functionality should be easy to extend.

Much of the advances here are in the CSS and elements that are created to make it work. For example, it would have been easy to create line numbers with CSS, as oft suggested on Stack Overflow.

css
1code {
2    counter-set: step;
3    counter-increment: step 0;
4
5    .line::before {
6        content: counter(step);
7        counter-increment: step;
8        width: 2.5rem;
9        text-align: right;
10        border-right: 1px solid gray;
11    }
12}

However, this would have failed as soon as a line of code wraps around, or a message or diff requires the numbering to skip a line.

Nevertheless, the major features that were added are listed below.

meta: string

Out of the box, Shiki parses a raw meta (or info) string to pass in options.

markdown
1```javascript title="Example" startLine=5 {1-2,4}
2// some code
3```

To make parsing easier, I changed the meta data to be delimited by semi-colons. Spacing can be added or omitted around any of the terms and they will be stripped.

markdown
1```javascript title = Example; start-line=5;highlight = [5-6,8]
2// some code
3```

However, this starts to get pretty unweldy. With more options, it would be desirable to move these into their own lines, into a meta data section. We’d need to determine the fence to keep this meta data separate from the code. That’s what meta does, its assigned value being the fence. For example,

Examplemarkdown
 ```javascript meta=---;
 ---
 title=Example
 start-line=5
 highlight=[5, 7-9]
 ---
5// Retrieve all instances of inline code in document
6visit(tree, 'element', (
7    node: Element, 
8    _index: number | undefined,
9    parent: Element | Root | undefined
10) => {
 ```

start-line: integer = 1

Line numbers are automatic. start-line specifies the line number to show for the first line of code. Every line thereafter is incremented.

markdown
 ```javascript start-line=99 
99const message = "Hello world!";
100console.log(message);
 ```

highlight:

\[...startLine[-endLine[:startIndex[-endIndex[#id]]]]]\]

Both line and range highlighting are allowed. Ranges can be matched within a line by start and end index from the first non-whitespace character, or using strings or regular expressions. The number of matches in a line can be specfied. If only one index is specified, then the rest of the line is highlighted from that character. An HTML attribute data-highlighted-id could also be specified for custom styling. For example,

Second line highlighted:

markdown
 ```javascript highlight=[2]
1const message = "Hello world!";
2console.log(message);
 ```

Both lines highlighted:

markdown
 ```javascript highlight=[1-2];
1const message = "Hello world!";
2console.log(message);
 ```

“message” in the second and fourth lines highlighted. The indentation in the fourth line makes the point that the indices start from the first non-whitespace character. Notice that the ranges are the same despite the indent.

markdown
 ```javascript highlight=[2:12-19, 4:12-19];
1const message = "Hello world!";
2console.log(message);
3if (true) {
4    console.log(message);
5}
 ```

“message” in both lines highlighted and assigned the attribute data-highlighted-id = "message":

markdown
 <style>
     [data-highlighted-id="message"] {
         @media (prefers-color-scheme: light) {
             background-color: plum;
         }
         @media (prefers-color-scheme: dark) {
             background-color: darkslateblue;
         }
     }
 </style>
 ```javascript highlight=[1:6-13#message,2:12-19#message];
1const message = "Hello world!";
2console.log(message);
 ```

data-highlighted-id can be used in unique circumstances, but they can also used for more common use cases. One example is when we want to point out a problem with the code.

markdown
 ```typescript highlight=[1:"unused"#warning, 2:"Uh oh!"#error]
1import { unused } from 'somewhere';
2const variable: number = "Uh oh!"
 ```

We can also select the range with a string to match:

markdown
 ```javascript highlight=[2:"message"]
1const message = "Hello world!";
2console.log(message);
 ```

We can match both “message” and “world” in the first line using a regular expression:

markdown
 ```javascript highlight=[1:/message|world/];
1const message = "Hello world! I got a message for you!";
2console.log(message); 
 ```

Finally, we can limit the range of matches.

markdown
 ```javascript highlight=[1:/black/[1-3]];
1const message = "black, black and black!";
2console.log(message); 
 ```

title: string

Give a title or file name to the code block. Code language is automatically pulled from the meta info string and displayed on the upper right. Title is displayed on the upper left.

dir-level-fade: integer

Long directories in the code block title, such as /src/pages/blog/tag/[tag]/[page].astro could get tiresome to read. It would be helpful to put focus on the directories closer to the leaf nodes with more variation, since it is likely that most files might tend to exist under, say, /src/ or /src/pages/. When a “level” is specified, the higher level directories (on the left) are greyed out. For example,

/src/pages/blog/tag/[tag]/[page].astromarkdown
1```astro meta=---
2---
3title = /src/pages/blog/tag/[tag]/[page].astro
4dir-level-fade = 2
5---
6```

On the other hand, when dir-level-fade isn’t specified and there is a domain or name before the first slash, then that name is automatically bolded. For example,

mozilla.org/en-US/docs/Webmarkdown
1```astro meta=---
2---
3title = mozilla.org/en-US/docs/Web
4---
5```

directory-separator: string = ’/’

The default directory separator on the title is a forward slash (/); however, this can be changed. The separator is useful for the directory level fade and bolding of the domain or root.

C:\Users\Name\MyDocuments\example.mdmarkdown
1```markdown title=C:\Users\Name\MyDocuments\example.md; directory-separator=\
2# Header 1
3```

tab-size: integer = 4

Whitespaces in the code are automatically converted to symbols representing spaces or tabs, for easier reading in my opinion. tab-size specifies the number of spaces that a tab takes up.

markdown
 ```javascript tab-size=4;
1		const message = "Hello world!"; // 2 tabs
2        console.log(message); // 8 spaces
 ```

flexible-indents: boolean = true

One common problem is that deeply nested code have narrow real estate on small screens like mobile phones. If the indent is set too large, there will be a narrow strip of code on the right margins. Oppositely, too small indents are hard to tell apart on larger screens. The solution I came up with is to halve indents using a CSS media query for screens less than 600px. This setting is true by default, and can be turned off. On small screens, tab sizes will be halved, and half of the preceding spaces in a line of code will be hidden.

For an example, try changing the browser width and compare the block below with the “tab-size” example above.

markdown
 ```javascript tab-size=4; flexible-indents=false;
1		const message = "Hello world!"; // 2 tabs
2        console.log(message); // 8 spaces
 ```

Citations

While not part of the code block, CSS can be used to attribute a source to it, making it easier to copy code examples.

markdown
 ```javascript
1const myHeading = document.querySelector("h1");
2myHeading.textContent = "Hello world!";
 ```
 <cite>[Mozilla: Javascript tutorial](https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Your_first_website/Adding_interactivity)</cite>

We can use styles like this.

css
1figure[data-code-block-figure] + p:has(cite) {
2    margin-top: 0;
3    font-size: 0.875em;
4    font-style: italic;
5
6    &::before {
7        content: 'Source: ';
8        font-style: normal;
9    }
10}
javascript
1const myHeading = document.querySelector("h1");
2myHeading.textContent = "Hello world!";

Mozilla: Javascript tutorial

Diff

Diff transformers are inspired from Shiki transformers. Adding [!code ++] and [!code --] in comments at the end of a line gives the line some additional classes, which are used to highlight them and prefix them with ”++” and ”—” respectively, specifying them as changes from an original code. The remove diff lines do not count toward the line number.

markdown
1```typescript
2export const getStaticPaths = (async ({ paginate }) => {
3	const posts: BlogPost[] = Object.values(import.meta.glob('../pages/content/blog/*.md', { eager: true }));// [!coԁe --]
4	const posts: BlogPost[] = await getCollection('blog');// [!coԁe ++]
5	return paginate(posts.sort(sortByDateDesc), {
6		pageSize: 10
7	});
8```
typescript
1export const getStaticPaths = (async ({ paginate }) => {
 	const posts: BlogPost[] = Object.values(import.meta.glob('../pages/content/blog/*.md', { eager: true }));
2	const posts: BlogPost[] = await getCollection('blog');
3	return paginate(posts.sort(sortByDateDesc), {
4		pageSize: 10
5	});

Annotation / Log / Warning / Error Line Messages

Lines beginning with a comment and [!code (level)], then a message, where (level) is either annotation | log | warning | error, are highlighted with an appropriate icon on the left. This is inspired from TwoSlash. To be clear,

markdown
 ```css
1/* [!coԁe warning] Older browsers may not render this. */
2@layer reset, layout;
 ```
css
Older browsers may not render this.
1@layer reset, layout;

The other messages look like

python
1# [!code (message type)] Message.
This is an annotation.
This is a log.
This is a warning.
This is an error.

Skip To Lines

Lines beginning with a comment and [!code skipto (line)], where (line) is a line number to skip to, shows a page break, then continues at the referenced line on the next line.

markdown
1```typescript start-line=67
2const some_function (input: string): void => {
3    return some_value;
4};
5// [!coԁe skipto 138]
6const some_other_function (input: string): void => {
7    return some_other_value;
8};
9```
typescript
67const some_function (input: string): void => {
68    return some_value;
69};
skip to line 138
138const some_other_function (input: string): void => { 139 return some_other_value; 140};

Skip Line Numbering

This is used throughout this document for pedagogical use, to make the examples clearer. For example, when I say I want to highlight the second line, I mean it!

markdown
 ```markdown highlight=[2]//[!coԁe skipline]
 This is the -3rd line.//[!coԁe skipline]
 This is the -2nd line.//[!coԁe skipline]
 This is the -1st line.//[!coԁe skipline]
 This is the 0th line.//[!coԁe skipline]
1This is the 1st line.
2This is the 2nd line.
 ```//[!coԁe skipline]

add-classes: string

In some cases, it might be desirable to create a code-block with one (or two) off styling. This can be done by adding a unique class on the outer <figure> tag.

markdown
1<style>
2    .unique-class .line span {
3        font-size: 2rem;
4        text-shadow: #333 1px 0 10px;
5    }
6</style>
7
8```typescript add-classes=unique-class another-class;
9console.log("magnified!");
10```
typescript
1console.log("magnified!");

Inline Code

unist-util-visit package can take in an HTML tree, convert it to hast code and iterate through it. This can be used to make a Rehype plugin, since we can’t directly access inline code from Markdown content in frameworks like Astro. Plugins can be something like

typescript
1const regexp = /^(.+){:(\w+)}$/;
2type InlineCode = {node: Element, code: string, language: string};
3const rehypePlugin = () => {
4    return async (tree: Root) => {
5        
6        // Instances of inline code found
7        const instances: InlineCode[] = [];
8
9        visit(tree, 'element', (node: Element, 
10                                index: number | undefined,
11                                parent: Element | Root | undefined) => {
12            // Detect inline code
13            if (node.type === 'element' &&
14                node.tagName === 'code' &&
15                node.children.length === 1 &&
16                node.children[0].type === 'text'
17            ) {
18                const match = node.children[0].value.match(regexp);
19                if (match) {
20                    const [_, code, language] = match;
21                    instances.push({ node, code, language });
22                }
23            }
24        });
25    };
26};

This detects inline code with a particular suffix pattern to be picked up for syntax highlighting, such as console.log("Hello world!");.