Word wrap in code with indent

Imagine we have code:

fn main() {
    println!("very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long line, {}", "hello world");
}

The default behavior for very long lines of code is just to add a scrollbar.

But if we add word-wrap to pre:

pre {
    white-space: pre-wrap;
    word-wrap: break-word;
}

Then code will look like this:

image

Which is not convenient to read.

I suggest to add an option to code, like linenos, but with the name indent-table, which will wrap each line in <table> with one <tr> and two <td>'s: first is just indent text (first spaces and tabs), second is remaining line text.

With that approach result will look like this:

image

Or there should be not a <table>, but another thing, I don’t know much HTML.

Oh, this meant to be Feature request. Can anyone please change category of this post, or I should recreate it?

So, I did it using JS.

Final result

How code looks:
image

How code selection looks:
image

When I copy this code and paste it into a text editor, the result is correct. How it looks in my Sublime Text:
image

Problems of the naive approach

At first, I tried to use <table> as I suggest in the original post

The only problem with using tables is copy-pasting: there was always \t symbol between indentation and start of pasted code. And this can’t be disabled, because this is how tables work.

Another approach — is to use flex containers, but it has the same problems, but now \n character is inserted between indentation and the code.

Make it work

In order to make this work, there must be two span's with spaces:

  • The first span is showed and used in the first column of the table for indentation, but it must have a style to disable selection (user-select: none;). I call this class .no-copy.
  • The second span lies in the second column, but must be hidden, and not by display: hidden, but using font-size: 0px, because in first approach the text is not copied, but in second it does.

Which tag to use

I suggested using <table>, but this is not an entirely nice solution, so this should be done using <div> with classes: .table, .tr, .td. Because with div’s + classes you don’t need to override default table behavior. And you can always create a table from classes using .display: table | table-row | table-cell.

Sublime-like additional indent

I used this css to make wrapped lines have additional indent:

.td:last-child {
  text-indent: -20px;
  padding-left: 25px;
}

On phones

Unfortunately, word-wrapping with indentation looks terrible on phones, so I disabled it using CSS on my blog.

My JS and CSS

This is my code to do all this thing. It’s not perfect but does the job. Whoever wants to use it, will need to tweak it by themself. Sorry, I don’t have much time to document all steps to add this code to another site.

function cloneAttributes(element, sourceNode) {
  let attr;
  let attributes = Array.prototype.slice.call(sourceNode.attributes);
  while(attr = attributes.pop()) {
    element.setAttribute(attr.nodeName, attr.nodeValue);
  }
}

window.addEventListener('DOMContentLoaded', () => {
	let re = /^[ \t]+/;
	document.querySelectorAll(".main-content pre code").forEach((code) => {
		let first_elem_in_line = true;
		let table = null;
		let tr = null;
		let td1 = null;
		let td2 = null;
		let span1 = null;
		let span2 = null;
		let new_code = document.createElement("code");
		new_code.classList = code.classList;
		cloneAttributes(new_code, code);
		new_code["data-lang"] = code["data-lang"];
		code.querySelectorAll("span").forEach((span) => {
			let string =  (' ' + span.innerHTML).slice(1);
			let last_elem_in_line = string.endsWith("\n");
			if (first_elem_in_line) {
				table = document.createElement("span");
				tr = document.createElement("span");
				td1 = document.createElement("span");
				td2 = document.createElement("span");
				span1 = document.createElement("span");
				span1_hidden = document.createElement("span");
				span2 = span.cloneNode();

				table.classList.add("table");
				tr.classList.add("tr");
				td1.classList.add("td");
				td2.classList.add("td");

				td1.classList.add("no-copy");
				span1_hidden.classList.add("hidden-copy");

				let matched = string.match(re);
				if (matched == null) {
					matched = [""];
				}

				span1.innerHTML = matched[0];
				span1_hidden.innerHTML = matched[0];
				span2.innerHTML = string.trimStart();

				td1.appendChild(span1);
				td2.appendChild(span1_hidden);
				td2.appendChild(span2);

				span1 = null;
				span2 = null;
			} else {
				let span3 = span.cloneNode();
				span3.innerHTML = span.innerHTML;
				td2.appendChild(span3);
			}

			if (last_elem_in_line) {
				tr.appendChild(td1);
				tr.appendChild(td2);
				table.appendChild(tr);
				new_code.appendChild(table);

				table = null;
				td1 = null;
				td2 = null;
				span1 = null;
				span2 = null;
			}
			first_elem_in_line = last_elem_in_line;
		});
		code.parentNode.replaceChild(new_code, code);
	});
});
// Breakpoints
$large-breakpoint: 64em !default;
$medium-breakpoint: 42em !default;

@mixin large {
  @media screen and (min-width: #{$large-breakpoint}) {
    @content;
  }
}

@mixin medium {
  @media screen and (min-width: #{$medium-breakpoint}) and (max-width: #{$large-breakpoint}) {
    @content;
  }
}

@mixin medium-large {
  @media screen and (min-width: #{$medium-breakpoint}) {
    @content;
  }
}

@mixin medium-small {
  @media screen and (max-width: #{$large-breakpoint}) {
    @content;
  }
}

@mixin small {
  @media screen and (max-width: #{$medium-breakpoint}) {
    @content;
  }
}

.main-content pre code {
  @include medium-large {
    .table { display: table; }
    .tr { display: table-row; }
    .td { display: table-cell; }
    .td:last-child {
      text-indent: -20px;
      padding-left: 25px;
    }
    .no-copy {
      -webkit-user-select: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      -o-user-select: none;
      user-select: none;
    }
    .hidden-copy {
      width: 0px !important;
      height: 0px !important;
      font-size: 0px !important;
    }
  
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    white-space: pre-wrap;
    white-space: -moz-pre-wrap;
    white-space: -pre-wrap;
    white-space: -o-pre-wrap;
    word-wrap: break-word;
    word-break: break-all;
  }

  @include small {
    .hidden-copy {
      display: none;
    }
  }
}

zola

This feature will be nice to have built-in in zola. So, after this research, I believe it’s more clear about all pros and cons of this feature.

I want try to make a PR if you find this feature acceptable.