Wrap indicator in <pre> blocks

10 May 2020

I am not a front-end developer, not a UI designer, nor a UX guru, but I am an engineer, so when I face a puzzle worth solving my brain switches on and I cannot let it go until I find a satisfactory solution to the puzzle.

My blog heavily relies on me sharing session dumps and file excerpts using the code blocks. I am using PrismJS to highlight syntax in these blocks. However, after a while I found that there is one thing that really irritates me: these code blocks are not designed to be truly responsive, for instance – when a line wraps there is no easy to spot indication that such a wrap happened.

I quick search on the topic highlighted that this is kind of an unresolved issue and I found just one blog post from 2012 by Ian Yang after I already implemented my solution.

The approaches are quite close, but I like mine better since I think it is more semantically correct (I am using <div> instead of <span> and I did a bit deeper research into how to make it universal).

So, below I present you with my version of the solution and I would appreciate any feedback you may have, especially if it could help to find a nicer solution in the end.

Before we dive into details on how I come up with the solution, feel free to play with the basic demo – try to resize the box by dragging the bottom right corner and see how the text inside responds (if you are using anything than the Chrome browser you may be out of luck with the resizable <div>s, sorry):

This is quite a long line. It is long enough to ensure that it will wrap no matter how big your screen is. Well, it is possible that in 20 years from the moment I type this humanity will invent a medium that could display the whole line with no breaks, but until then it should be good enough for the demonstration purposes.
A very short line.
The short line above is used to demonstrate that it stays clear of additional info when its neighbour lines are wrapped.

I hope that demo gods were not angry at me and the demo above worked for you as expected, so if you want to understand how it works let’s dive under the hood of this solution :)

Preparation

First of all, we need to define what we have and what we want to achieve – it helps to stay on course, to understand when we reached a solution, and to assess how good the solution is:

  • we have one or more code blocks expressed through the <pre> HTML element;
  • these code blocks may have continuous white space that we want to preserve;
  • our page layout is flexible and there are no guarantees that the width of a particular viewport (a window through which a browser renders the visible part of the page) is enough to display the whole line of code;
  • we want to ensure that layout does not break when a very long code line is encountered, so we expect that line to be wrapped to the next line upon hitting width of the containing box;
  • we need to present the reader (a user who is consuming information) a visual indicator that the line was wrapped;
  • we also want the syntax highlighter (such as PrismJS in my case) not to be affected by whatever we come up with.

I think the above quite a solid definition of the goals and requirements for our little project. It is time to assess the artefacts we have and what we can do with them.

Analysis

Our primary artefact is the <pre> element. It contains the information we want to share with the world and we want to do this in the most easy to digest way.

By default, the <pre> element is quite a simplistic container. Its semantic meaning dictates that the content preserves the original formatting (such as amount and type of white space on the lines). The element also provides some basic functionality: you can define the size of the container and browsers are supposed to present you with controls to scroll the content if it does not fit the size of the container.

The following is just such bare almost not styled <pre> block wrapped into a resizable <div> container, so you could play with the dimensions of the container (in order to preserve the layout I am keeping the wrapping styles, so the box would fit into the layout):

This is an example of a basic <pre> container.  You should
be able to resize it by dragging the bottom right corner (but if you are
using anything other than Chrome you may be out of luck with the resizing).

This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

However, if you played with the above container you may have noticed that at some point all lines are so mixed up together and it is really hard to tell that the original text comprised of five separate lines.

Unfortunately, the CSS standards do not provide any selectors or pseudo-elements to anchor the soft breaks. All we can do with the CSS selectors on the contained text is to choose the first letter (with :first-letter) and the first line of the text inside the <pre> element (with :first-line).

This is not good enough if you ask me. I would love to see some CSS selectors for soft breaks in the future iterations of the standard, but we are not there yet, so we have to work with what we have got.

And all we have is the standard CSS basic box model presentation focused selectors such as ::before and ::after.

So, if we look how text is rendered inside the <pre> block (browser’s DevTools is a really good tool to do the observations, by the way) we will notice that the browser considers the entire text as a single entity – there are no lines or anything, just a blob of text.

Therefore, the first piece of the puzzle would be to introduce something that could make the distinction between different lines of text possible.

Assuming that we can solve the above issue with introducing lines, the next step would be to display a marker on the right side of the line. That marker should follow the height of the line box as the line wraps.

Finally, we will need to figure out how not to display the marker on the last line of the wrapped multiline text to denote the end of wrapped line.

Implementation

Since the definition of the <pre> element only stipulates that the content of the element is pre-formatted with white space we can use other tags inside, e.g. we can wrap each physical line (a line that is terminated by a newline character) in a block element such as <div>.

Let’s see how it looks on our sample <pre> block when we wrap each physical line in <div class="line"></div>. Keep in mind that <div> is a block element, so in order not to introduce unintended line breaks we need to ensure that every newline character in the original text except the very first one is replaced with the combination of the closing and opening tags such as </div><div class="line">, below is the result of such a change on our sample:

This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

Visually, it is almost exactly the same as the original sample we presented in the “Analysis” section, however there is a couple of differences.

The first one is visual and is obvious: the empty line (the fourth line of the sample) has collapsed into nothing. This is an undesired side effect and the reason for that is that the corresponding <div> block has no content, hence it has zero height. We can fix it by defining the min-height attribute for out <div>s defining line boundaries.

The second difference is not so obvious, but if you inspect any line of this <pre> block using your browser’s DevTools you will see that now we can differentiate between lines (when you walk through the DOM in your DevTools your browser will highlight the corresponding line inside the <pre> block and this is what we wanted to achieve at this step!

The next step is to display a marker on the right side of the line and that marker should occupy the exactly the same height as the line block we are attaching it to. Well, this can be easily done with the ::after pseudo-element:

pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1em;
background-color: blue; /* temporary, to make it visible */
}

Note that we also introduced position: relative to our <div>‘s. It is needed since if we want to position our pseudo-element relative to its parent, the parent needs to have the position attribute set to anything but static. In our context, relative is the desired positioning for the <div> element. The corresponding rendered sample with the above stylesheet applied follows:

This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

So far so good! We have a blue coloured strip hanging on the right side just next to our text. Let’s style it a bit so it looks more appealing.

We have a couple of options here:

  1. we can just use a transparent bitmap image with the desired marker;
  2. we can leverage SVG as the source of the image.

The issue I have with transparent bitmap images is that they are not very responsive: to accomodate to all the possible resolutions I would end up with creating many bitmaps of different sizes to ensure that the marker looks good no matter what device is rendering the page.

On the other hand, the SVG graphics was not well supported on some browsers like Internet Explorer 6, etc. – The support is much better now, hence I would choose SVG everytime.

Ideally, I did not want to use an image, but I could not figure out how I could put a repeating text block next to my lines, hence I found the following compromise (this is the content of the SVG file I created):

<svg xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 24 24">
<text x="0" y="19">&#x23ce;</text>
</svg>

The first two lines are mandatory and describe to a browser that it works with an SVG image file.

The third line (viewBox=) is really important since it allows the SVG image to be scaleable, basically it says that the dimensions of the image are 25 by 25 units (the units are abstract, but you may think of them as pixels if it makes it easier).

Finally, the <text> element puts a text box with just a single Unicode character for the return symbol (⏎), since I think it is quite an adequate symbol to represent a line wrap point.

There are different ways how you can incorporate an SVG image into CSS rule, but since the content of the SVG file is so small and I did not want to host it externally (it would be an additional fetch request and more maintenance), I just did the following:

[user@localhost ~]$ cat nl.svg
<svg xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 24 24">
<text x="0" y="19">&#x23ce;</text>
</svg>
[user@localhost ~]$ sed -n '1{h;d};$!H;${H;x;s,\s\+, ,g;s,>\s*<,><,g;p}' nl.svg | base64 -w 0 ; echo
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==

In other words, I removed all the white space, then deleted all line breaks, so I got a single line SVG body, and I converted it to Base64. The resulting line is usable as the inline definition of an image. Let’s update our stylesheet with the new values (we just replaced the last line to change the background to background-image) and added dimensions for the background itself:

pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1em;
background-image:
url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==)
no-repeat repeat;
background-size: 1em 1em;
}
This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

Look at that, it is almost shaping into what we want :). However, something is not right – the alignment of the markers to the corresponding lines is nowhere to be seen. It is, like, they are of different sizes despite that the font-size property for both of them is the same. Hmm.

If we recall that CSS basic model document we will discover in the very last paragraph that:

note that for non-replaced inline elements, the amount of space taken up (the contribution to the height of the line) is determined by the line-height property

Is this our hint? Let’s try to explicitly set the line-height property for our lines (reading documentation you may find that the default value is normal and you can also find that normal equals 1.2 for the majority of browsers, but relying on that, I think is a bad idea). We also need to adjust our markers with the new information we have got:

pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1.2em;
background-image:
url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==)
no-repeat repeat;
background-size: 1.2em 1.2em;
}
This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

This is an improvement, is not it? So we just need some final touches to call it done:

  • As it stands, we are showing the marker for the last part of the wrapped line, but we should not;
  • The colour of the marker is too bright and matches the text colour of the line wrapping of which we are highlighting.

To address the latter a simple play with the opacity attribute will suffice, e.g. I think 50% transparency should make the marker less distracting.

The former, however, requires some thinking. Given that the post is already too long, I will just state that out of multiple options I had I chose the following: raise the background image by one line-height from the bottom, and clip a square 1x1 line-height at the top to compensate, as follows:

pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
bottom: 0;
right: -1.2em;
background-image:
url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==)
no-repeat repeat;
background-size: 1.2em 1.2em;
margin-bottom: 0;
clip: rect(1.2em 1.2em 100vh 0);
opacity: .5;
}

The only tricky part in the added lines is the clip: rect(1.2em 1.2em 100vh 0) line. The clip property defines the visible rectangle the browser should preserve, but there is a conundrum: the height of the wrapped line is variable, so we cannot deterministically set this. The trick here is that it is highly unlikely (on the border of completely impossible :) ) that the visible height of the wrapped line would be bigger that the height of the viewport the user is looking through.

Therefore, we are setting the height of the clipped area to the height of the viewport (100vh), but this will be a rare edge case to actually utilise this – in the majority of cases the height of the clipping rectangle will be limited by the height of the block containing the wrapped line. Basically, this is exactly what we want :).

Let’s see the final result in action:

This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

Bonus Round

It should be mentioned, it is also possible to apply the same approach to the left side of the lines with a bit of a twist.

pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
text-indent: -1.2em; /* first line indent */
padding-left: 1.2em; /* padding for the line */
}
pre div.line::before {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
left: 0;
background:
url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==)
no-repeat repeat;
background-size: 1.2em 1.2em;
clip: rect(1.2em 1.2em 100vh 0);
opacity: .5;
transform: rotateY(180deg); /* mirror the arrow */
transform-origin: left;
}

The trick here is the play between padding-left and text-indent for the <div> element since text-indent will be applied to the first line only (effectively negating the padding set for the whole element), thus the rest of wrapped line will be padded to make space for the marker, which we place as usual with the clipping region to suppress it for the very first part of the wrapped line.

The result looks as follows (and I am actually torn which side I like better, so, maybe, for my blog I may just go with the left-side solution):

This is an example of a basic <pre> container. You should
be able to resize it by dragging the bottom right corner (but if you are
using anything than Chrome you may be out of luck with the resizing).
This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)

This idea for the padding-left and text-indent trick came from C. Shaun “Kainaw” Wagner when he answered a question about the wrap indicators on Stack Overflow (I am sure that it was known before, but this is where I learnt it).

Assessment of the result

We started with the following requirement:

  • these code blocks may have continuous white space that we want to preserve;

    Achieved: we did some changes to ensure that even empty lines are preseved.

  • our page layout is flexible and there are no guarantees that the width of a particular viewport (a window through which a browser renders the visible part of the page) is enough to display the whole line of code

    Achieved: our blocks are flexible and if a line is too long it is wrapped in an easy to understand way. We proved it by ensuring that any block can be dynamically resized.

  • we want to ensure that layout does not break when a very long code line is encountered, so we expect that line to be wrapped to the next line upon hitting width of the containing box;

    Achieved: ditto as the previous item.

  • we need to present the reader (a user who is consuming information) a visual indicator that the line was wrapped;

    Achieved: we got our dynamic markers showing when the line was wrapped.

  • we also want the syntax highlighter (such as PrismJS in my case) not to be affected by whatever we come up with.

    Unknown: I think it would be the material for another post, but I do not expect a lot of issues since we only replaced the newline characters and preserved the rest of lines intact.i

    It turned out, that it was quite a hassle since none of plugins PrismJS have for working with lines expect semantic markup of the lines inside the block. Therefore, I spent some time and came up with a plugin that does it “right”.

    It is highly likely, that my plugin will get merged into the Prism tree (see the corresponding Pull Request). After a discussion with upstream, it seems they want to rework a large chunk of the project to use the proposed functionality and make it a library of some sort. I do not have time to actively see it through, so one of Prism developers will continue the integration of my idea into Prism and something comparable will be implemented in Prism in near future. However, if for some reason it would not be or you need the functionality “here & now”, you can always get it from my fork of PrismJS (I will keep the plugin-lines branch until it is merged upstream upstream decides how to incorporate my idea).

    By the way, my Lines plugin is the primary working horse for all syntax highlighted blocks on this site (so you have already seen it in action).

I hope this helped you and you learnt something :). If you have any constructive feedback, I would appreciate it, specifically if you have ideas on how to improve the presented solution.