Ленивая подсветка синтаксиса кода с помощью highlight.js

В этой статье рассмотрим, как подключить highlight.js к сайту и настроить с помощью этого инструмента подсветку синтаксиса кода. Для реализации ленивого выполнения highlight.js воспользуемся Intersection Observer API.
Исходный код
Файлы решения для отложенной подсветки синтаксиса кода, расположенного в элементах <pre><code>...</code></pre>
, находятся на Github.
Для вставки данного решения на страницу к ней необходимо подключить файлы main.css и main.js:
<link href="assets/css/main.css" rel="stylesheet">
<script defer src="/assets/js/main.js"></script>
Скрипт «main.js» в свою очередь добавляет в документ ещё два файла «highlight.min.css» и «highlight.min.js». Настроить их расположение можно посредством редактирования следующих строк в «main.js»:
style.href = 'assets/css/highlight.min.css';
// ...
script.src = 'assets/js/highlight.min.js';
Эти файлы загружены со страницы: https://highlightjs.org/download/

Пример подсветки JavaScript кода с помощью «highlight.js»:
<pre><code class="lang-js">function sum(a, b) {
return a + b;
}
console.log(sum(5, 7));</code></pre>

Описание процесса реализации ленивой подсветки синтаксиса кода
Задачу по ленивой подсветки синтаксиса кода, содержащегося в <pre><code>...</code></pre>
, выполним с помощью «highlight.js» и Intersection Observer API.
Для этого сначала напишем две функции:
loadCSSandJS()
– для динамического подключения «highlight.js» к странице;highlight()
– для подсветки контента элементов, находящихся в массивеelements
.
После этого напишем код с использованием Intersection Observer API, который будет запускать уже созданные функции, когда <pre><code>
будет приближаться к области просмотра.
Этап 1. Подсветка синтаксиса с помощью highlight.js
Подсветку синтаксиса кода реализуем с помощью highlight.js.
«highlight.js» - это инструмент, который позволяет выполнять раскраску кода внутри тегов <pre><code>
. Он может это делать и в браузере, и на сервере. Если язык явно не указать, то он попытается его определить автоматически.
Ручное указание языка осуществляется с помощью добавления класса:
<pre><code class="hljs language-html">
...
</code></pre>
Для использования «highlight.js» на веб-странице необходимо подключить стили и скрипт.
Обычно это осуществляется так:
<link rel="stylesheet" href="assets/css/highlight.min.css">
<script src="/assets/js/highlight.min.js"></script>
Но мы будем это делать динамически. Для этого напишем следующую функцию, которую будем вызывать когда <pre><code>
будет находиться на расстоянии 200px до области просмотра (viewport).
// переменная, которая говорит о том подключен ли «highlight.js» к странице
let hasLoaded = false;
// функция, в которой напишем код для подсветки синтаксиса, используя методы highlight.js
const highlight = () => { ... }
// функция для динамического подключения highlight.js к странице
const loadCSSandJS = () => {
// вставляем стили
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'assets/css/highlight.min.css';
document.head.appendChild(style);
// вставляем скрипт
const script = document.createElement('script');
script.src = 'assets/js/highlight.min.js';
script.async = 1;
document.head.appendChild(script);
script.onload = () => {
// после загрузки скрипта присваиваем переменной hasLoaded значение true и вызываем функцию highlight для подсветки синтаксиса
hasLoaded = true;
highlight();
}
}
Для подсветки синтаксиса воспользуемся методами hljs
.
Когда язык установлен, то будем вызывать метод highlight
:
// item - элемент (<pre><code>)
// lang - язык
hljs.highlight(item.textContent, { language: lang })
В противном случае highlightAuto
:
// item - элемент (<pre><code>)
hljs.highlightAuto(item.textContent)
Реализуем функцию highlight
следующим образом:
// элементы, контент которых нужно подсветить
const elements = [];
// функция highlight
const highlight = () => {
// скопируем elements в переменную items
const items = elements.slice();
// очистим массив elements
elements.length = 0;
// переберём каждый <pre><code>
items.forEach(item => {
// определим, имеется ли в элементе класс lang-*
let lang = false;
item.classList.forEach(className => {
if (className.indexOf('lang-') === 0) {
lang = className.replace('lang-', '');
}
});
// если класс имеется, то вызовем highlightAuto, иначе highlight
const result = lang ? hljs.highlight(item.textContent, { language: lang }) : hljs.highlightAuto(item.textContent);
// установим в качестве содержимого элемента HTML код, возвращённый highlight.js
item.innerHTML = result.value;
});
}
Этап 2. Организация ленивого выполнения функции для раскраски кода
Подсветку синтаксиса кода будем выполнять не сразу после загрузки документа, а только в тот момент, когда элемент <pre><code>
будет приближаться к области просмотра.

Наблюдать за изменением видимости <pre><code>
будем с помощью Intersection Observer.
Чтобы это сделать, необходимо создать объект Intersection Observer:
const observer = new IntersectionObserver(cb, params);
Конструктору IntersectionObserver
в круглых передадим 2 аргумента:
- функцию обратного вызова
cb
, которая будет выполняться всякий раз, когда отслеживаемые<pre><code>
будут пересекать viewport; - настройки отслеживания
params
.
Функция обратного вызова cb
:
// entries - массив объектов, каждый из которых представляет один превышенный порог, который становится более или менее видимым, чем процент, указанный этим порогом.
const cb = (entries, observer) => { ... }
Настройки отслеживания:
// root используется для указания корневого объекта (области просмотра); для установления в качестве него viewport, нужно присвоить этому ключу значение null
// rootMargin позволяет определить поля для root; зададим «0px 0px -200px 0px», чтобы пересечение считалось ниже области просмотра на 200px.
const params = {
root: null,
rootMargin: '0px 0px 200px 0px'
}
Добавим элементы, за которыми необходимо наблюдать в observer
. Это осуществляется с помощью метода observe
:
document.querySelectorAll('pre>code').forEach(item => {
observer.observe(item);
});
Функцию обратного вызова реализуем следующим образом:
// entries - массив объектов IntersectionObserverEntry, каждый из которых представляет один целевой элемент
// observer - объект Intersection Observer, для которого вызывается функция обратного вызова
const cb = (entries, observer) => {
// переберём все IntersectionObserverEntry
entries.forEach(entry => {
// если цель пересекла root
if (entry.isIntersecting) {
// получим целевой элемент
const target = entry.target;
// добавим целевой элемент в массив elements
elements.push(target);
// прекращаем наблюдение за этим целевым элементом
observer.unobserve(target);
}
});
// если в массиве elements имеются элементы
if (elements.length) {
// если highlight.js подключен к странице
if (hasLoaded) {
// то выполним подсветку контента элементов, вызвав для этого функцию highlight
highlight();
} else {
// иначе подключим highlight.js к странице и выполним подсветку элементов, находящихся в массиве elements
loadCSSandJS();
}
}
}
В ней мы пробежимся по целевым элементам и добавим в массив elements
те из них, которые пересекли область просмотра. Далее выполним их раскраску.