blog.itcode.devblog.itcode.dev

[NextJS] Blog Overhaul Journey - 5. Improving Code Block Design Using marked

In the previous chapter, I implemented this blog's own Markdown converter using marked. Let's use this converter to make the plain code blocks look a bit more like an IDE.

[NextJS] Blog Overhaul Journey - 5. Improving Code Block Design Using marked

In the previous chapter, I implemented this blog's own Markdown converter using marked. Let's use this converter to make the plain code blocks look a bit more like an IDE.
RWB0104
@RWBwritten at 2021-11-07 12:13:57
Blog Overhaul Journey

시리즈 모아보기

Blog Overhaul Journey

2 / 2

In the previous chapter, I implemented this blog's own Markdown converter using marked. Let's use this converter to make the plain code blocks look a bit more like an IDE.

Markdown uses backticks to write code blocks. There are two types of code blocks: inline and block.

Inline code blocks are denoted using a single backtick, and have the following characteristics.

  • Can be inserted inline in the middle of text and continued on
  • Can be used in any context, such as lists or tables

An example of an inline code block looks like this.

Block code blocks are denoted using three backticks, and have the following characteristics.

  • Occupy an entire space on their own, as a block
  • Since they occupy space, they can't be continued inline in the middle of text, or used in lists or tables
  • They support specifying a language such as html or markdown, allowing detailed code representation per language

JAVASCRIPT

const test = 'block codeblock test';

alert(test);

That's an example of a block code block. On sites that support Markdown, such as GitHub, code blocks often support syntax highlighting for the corresponding language.

Of the inline and block types, let's improve the design of block code blocks. Unlike inline blocks, block code blocks occupy an entire section as a block and display code, so there are quite a few elements to design.

The items to improve are as follows.

  • Change the basic design frame
  • Display the language used (JAVA, C#, etc.)
  • Add a copy-to-clipboard button
  • Display line numbers
  • Distinguish lines with alternating colors
  • Implement highlighting of a line on mouse hover

Let's add these features one by one.

Let's change the plain design of the code block. I based it on a code block design I thought looked nice, one I'd seen on a foreign blog.

I originally wanted to design it while looking directly at the reference, but when I actually went to find it again, I couldn't, so I designed it based on the vague memory I had left of it.

As shown above, it takes the form of a window, with a Macintosh-style window context attached at the top left.

I could use an image, but I prefer to implement things with tags wherever possible, or with SVG if tags really won't do. In this case, since there's no complex pattern, it seems like everything can be implemented at the HTML tag level.

That's the layout to apply. Expressed in HTML, it looks like this.

HTML

<div>
	<!-- Header -->
	<div>
		<div><!-- Red button --></div>
		<div><!-- Yellow button --></div>
		<div><!-- Green button --></div>
	</div>

	<pre>
		<!-- Code content -->
	</pre>
</div>

Let's change the renderer so it renders code blocks with the design above.

In marked, the renderer for block code blocks is defined as renderer.code. You just need to override the function on that object.

ParameterTypeRequiredDescription
codestringYCode
langstringNLanguage
TypeDescription
stringRendering result

The parameters and return value definition for renderer.code are as shown in the tables above. You just need to write a function that fits these definitions.

TYPESCRIPT

loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]);

const renderer = new marked.Renderer();

// Render code block
renderer.code = (code: string, lang: string | undefined): string =>
{
	// If a valid language is present
	if (lang && renderer?.options?.highlight)
	{
		code = renderer.options.highlight(code, lang as string) as string;

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<div></div>
					<div></div>
					<div></div>
				</div>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}

	// If not
	else
	{
		lang = 'unknown';

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<div></div>
					<div></div>
					<div></div>
				</div>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}
};

By overriding the function like this, we change the rendering result of block code blocks.

If a language is provided along with the code, PrismJS highlighting is applied; if no language is provided, the text is placed in the code block as-is.

Set the style code so each tag is positioned according to the layout.

SCSS

$fd: 18px;
$fm: 14px;

pre[class*="language-"],
code[class*="language-"] {
	@include gutter;

	background-color: #161d2c;
	padding: 55px 20px 20px 20px !important;

	border-radius: 10px;

	font-family: Hack, AppleSDGothicNeo, sans-serif;
	color: white;

	text-align: left;
	white-space: pre;
	word-spacing: normal;
	word-break: normal;
	word-wrap: normal;

	-webkit-hyphens: none;
	-moz-hyphens: none;
	-ms-hyphens: none;
	hyphens: none;

	overflow: auto;
}

code:not([class*="language-"]) {
	color: white;

	font-family: Hack, AppleSDGothicNeo, sans-serif;
	font-size: $fd - 4px;

	display: inline-block;

	padding: 0px 4px;
	margin: 0px 3px;

	border-radius: 5px;

	@media (max-width: 960px) {
		font-size: $fm - 4px;
	}
}

.codeblock {
	position: relative;

	.top {
		position: absolute;

		top: 0px;
		left: 0px;

		width: 100%;
		padding: 5px 20px;

		background-color: #2b3445;

		border-top-left-radius: 10px;
		border-top-right-radius: 10px;

		display: flex;
		flex-direction: row;

		align-items: center;

		div {
			width: 15px;
			height: 15px;

			border-radius: 50%;

			margin: 0px 5px;

			&:nth-child(2) {
				background-color: #fe5f57;
			}

			&:nth-child(3) {
				background-color: #ffbd2e;
			}

			&:nth-child(4) {
				background-color: #29c941;
			}
		}
	}
}

That's the style assigned to the layout.

Displaying the language used will make it easier for readers to identify the code block's language, and will also save the author the trouble of manually explaining the code each time.

In renderer.code, the renderer function for block code blocks, the language used is assigned to the lang parameter. We can use this parameter to figure out the language used and put it to good use.

Based on the design above, it seems like a good idea to display the language in the header area.

TYPESCRIPT

loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]);

const renderer = new marked.Renderer();

// Render code block
renderer.code = (code: string, lang: string | undefined): string =>
{
	// If a valid language is present
	if (lang && renderer?.options?.highlight)
	{
		code = renderer.options.highlight(code, lang as string) as string;

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}

	// If not
	else
	{
		lang = 'unknown';

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}
};

Set it up so the language used is displayed in uppercase within the div.top area.

You can also tweak the design if needed. I only changed the font color.

SCSS

.top {
	/* top scss omitted */

	div {
		width: 15px;
		height: 15px;

		border-radius: 50%;

		margin: 0px 5px;

		p {
			margin: 0px;
			flex-grow: 1;

			color: map-get($yellow, "400");
		}

		&:nth-child(2) {
			background-color: #fe5f57;
		}

		&:nth-child(3) {
			background-color: #ffbd2e;
		}

		&:nth-child(4) {
			background-color: #29c941;
		}
	}
}

Add the style for the p tag.

Most code blocks on developer-friendly sites provide a button for copying the code block's content. This lets users easily copy the code block's content without having to drag-select the entire text.

We just need to add a button when rendering block code blocks, and add a script specifying that the code block's content is copied on the click event.

HTML

<button onclick="copyCode(this);">
	<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard">
		<path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path>
	</svg>
</button>

That's the layout. One button is added. When the button is clicked, a script is included that finds the code block included within the button's layout and saves its content to the clipboard.

The button's icon uses SVG.

JAVASCRIPT

/**
 * Code copy function
 *
 * @param {DOMElement} dom: HTML DOM
 */
function copyCode(dom)
{
	window.getSelection().selectAllChildren(dom.parentElement.querySelector('pre'));
	document.execCommand('copy');

	const origin = dom.innerHTML;
	dom.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-icon="check" class="i-check"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>';

	setTimeout(() => dom.innerHTML = origin, 1000);
}

The code-copy method, copyCode, is composed as shown above. It finds the nearest pre tag above the copy button, and copies its content. It also changes the button's SVG to a check icon for about 1 second.

SCSS

button {
	position: absolute;

	top: 50px;
	right: 20px;
	width: 40px;
	height: 40px;

	background-color: #1e2739;
	cursor: pointer;

	border: 1px solid map-get($grey, "600");
	border-radius: 10px;

	opacity: 0;

	transition: 0.5s;

	&:hover {
		transition: 0.5s;
	}
}

That's the style code. I chose an absolute-based layout so the button always appears in a fixed position.

Based on this, let's add it to the renderer's code block rendering process.

TYPESCRIPT

loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]);

const renderer = new marked.Renderer();

// Render code block
renderer.code = (code: string, lang: string | undefined): string =>
{
	// If a valid language is present
	if (lang && renderer?.options?.highlight)
	{
		code = renderer.options.highlight(code, lang as string) as string;

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<button onclick="copyCode(this);">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard">
						<path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path>
					</svg>
				</button>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}

	// If not
	else
	{
		lang = 'unknown';

		const langClass = 'language-' + lang;

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<button onclick="copyCode(this);">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard">
						<path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path>
					</svg>
				</button>

				<pre class="${langClass}">
					${code}
				</pre>
			</div>
		`;
	}
};

The button is now added to the code block.

Sometimes when explaining code, you need to point out a specific part of it. In this case you'd normally guide the reader to a particular line, but if the code block doesn't display line numbers, the user has to search through the code manually to find the line, which is a hassle. If the code is long enough to span multiple pages, the inconvenience doubles, and this can even end up lowering the quality of the content.

Let's display line numbers to better help users read the code.

HTML

<table>
	<tbody>
		<tr>
			<td><!-- Line number --></td>
			<td><!-- Code --></td>
		</tr>

		<tr>
			<td><!-- Line number --></td>
			<td><!-- Code --></td>
		</tr>

		<tr>
			<td><!-- Line number --></td>
			<td><!-- Code --></td>
		</tr>

		<tr>
			<td><!-- Line number --></td>
			<td><!-- Code --></td>
		</tr>
	</tbody>
</table>

The line number must be the same size as the code line. If there's even a slight pixel difference, the more code lines there are, the more the misalignment will accumulate.

To match this precisely, I adopted a table-based layout. Each tr tag represents one line, and the two td tags below split it into a line number and a code area. Making good use of the table's characteristic that td elements within the same tr sit on the same row means the layout CSS doesn't need much effort.

SCSS

table {
	border-collapse: collapse;

	& td {
		line-height: $fd + 4px;

		@media (max-width: 960px) {
			line-height: $fm + 4px;
		}
	}

	& td:nth-child(1) {
		color: #455983;
		padding-right: 10px;

		border-right: 1px solid #455983;

		text-align: right;

		user-select: none;
		-moz-user-select: none;
		-webkit-user-select: none;
	}

	& td:nth-child(2) {
		width: 100%;

		padding: 0px 20px 0px 10px;
	}
}

That's the design. The number is displayed in the first td, and the border-right property is used to show a dividing line. Let's change the code block renderer based on this.

TYPESCRIPT

loadLanguage([ 'javascript', 'typescript', 'java', 'html', 'css', 'json', 'scss', 'sass', 'sql', 'batch', 'bash' ]);

const renderer = new marked.Renderer();

// Render code block
renderer.code = (code: string, lang: string | undefined): string =>
{
	// If a valid language is present
	if (lang && renderer?.options?.highlight)
	{
		code = renderer.options.highlight(code, lang as string) as string;

		const langClass = 'language-' + lang;

		const line = code.split('\n').map((item, index) => `
			<tr data-line=${index + 1}>
				<td class="line-number" data-number="${index + 1}">${index + 1}</td>
				<td class="line-code" data-number=${index + 1}>${item}</td>
			</tr>`).join('\n').replace(/\t|\\n/, '');

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<button onclick="copyCode(this);">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard">
						<path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path>
					</svg>
				</button>

				<pre class="${langClass}">
					<table>
						<tbody>${line}</tbody>
					</table>
				</pre>
			</div>
		`;
	}

	// If not
	else
	{
		lang = 'unknown';

		const langClass = 'language-' + lang;

		const line = code.split('\n').map((item, index) => `
			<tr data-line=${index + 1}>
				<td class="line-number" data-number="${index + 1}">${index + 1}</td>
				<td class="line-code" data-number=${index + 1}>${item}</td>
			</tr>`).join('\n').replace(/\t|\\n/, '');

		return `
			<div class="codeblock">
				<div class="top">
					<p>${lang.toUpperCase()}</p>
					<div></div>
					<div></div>
					<div></div>
				</div>

				<button onclick="copyCode(this);">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard">
						<path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path>
					</svg>
				</button>

				<pre class="${langClass}">
					<table>
						<tbody>${line}</tbody>
					</table>
				</pre>
			</div>
		`;
	}
};

Since each tr tag is attached one by one via map, we can determine the line number during this process. I set it up so the map index, index, is assigned to the first td. To strengthen the tags' semantic meaning, the data-number attribute of each tr and td is likewise assigned the index. Note that the index starts at 0.

For example, line 126 would be rendered as follows.

HTML

<tr data-number="126">
	<td data-number="126">126</td>
	<td data-number="126"><!-- code --></td>
</tr>

When reading densely packed text on the internet, it's easy to lose track of where you are. Code in particular tends to be even more confusing because, by nature, the meaning of each line is very sparse and complex.

To reduce this fatigue, alternating colors per line can help lower the fatigue of the reading process. I'll use the traditional method of assigning different colors to odd and even lines.

Fortunately, we're already managing each line with a tr tag in order to display line numbers. That means we just need to assign different colors to odd tr elements and even tr elements. Using CSS's nth-child selector, this can be solved very simply.

SCSS

table {
	& tr:nth-child(2n) {
		background-color: #1c2335;
	}
}

For the color, I chose a shade slightly lighter than the existing background color, so that it would blend well with the existing code block design. The even-numbered lines will be assigned a slightly lighter color.

Let's add one more interactive feature to the code block. When the mouse hovers over a code line, that line gets highlighted. By hovering over the code block, users can boost the readability of the code they're reading, and this interaction with the code block can also help draw more interest to the content.

Just like before, this can be solved purely with CSS, using the :hover selector.

SCSS

table {
	& tr:hover {
		background-color: #546687;

		& td:first-child {
			color: white;
		}
	}
}

This should be enough. When you hover over a tr tag, the background color of that line and its line number change to a brighter color. Adding the transition property lets you get a smooth visual effect.

During the blog overhaul, the code block was an area I paid particular attention to. I'm glad the design and functionality turned out about as well as I'd envisioned. Since applying marked, I've made full use of the ability to freely customize the rendering process.

Next, let's apply LaTeX to express mathematical formulas.

# NextJS# React# Markdown# HTML# SCSS# TypeScript
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08