Подсвечиваем НЕ ЛАТИНСКИЕ СИМВОЛы в коде и тексте

У меня учится много начинающих разработчиков. Очень часто они спрашивают "почему не работает, все за вами повторил" и посылают код. Этот вопрос так часто повторяют, что можно вести статистику ошибок. Одной из самых коварных ошибок, является написание не латинских символов в коде. При такой ошибке консоль не показывает error и ничего не работает. После того, как найдена ошибка, студенты просят показать, как я ее нашел. Как правило, все просто. Если код не работает, ошибок в консоли нет, и визуально все должно работать, следует проверить код на наличие не латинских символов. Действительно, давайте посмотрим на две строчки:
const out = document.querySelector('.out-1');
const out = document.querySelеctor('.оut-1');
Зрительно – они одинаковые. На самом деле, во второй строке написано два символа на кириллице. Один это e, второй o. Выявить их визуально очень тяжело. Выход – либо запускать редактор HEX и смотреть код, либо использовать онлайн способы проверки. Вы скажете, что редакторы должны подсвечивать такое? Нет не всегда.
Сегодня мы напишем программу JavaScript, которая позволит подсвечивать в тексте не латинские символы. Усложним разработку дополнительных требований:
- напишем двумя способами: с помощью строки и с помощью регулярных выражений,
- форматирование текста должно сохраняться (не будем делать кашу),
- программа должна проверять не только тексты, но и код программ,
- код после проверки должен работать, если его скопируют и вставят в редактор!
Думаю достаточно. Уехали. Для начала верстка.
Верстка приложения проверки не латинских символов
Верстка состоит из трех элементов:
- textarea с классом или id, куда будет вводиться текст,
- кнопка, запускающая функцию нахождения не латинских символов,
- div, куда будем выводить результат текста.
Сделаем верстку:
<textarea id="latin-text"></textarea>
<button>Search no latin symbols</button>
<div class="latin-result">
<pre><code></code></pre>
</div>
Обратите внимание, что для вывода мы используем конструкцию pre > code. Это позволит упростить визуализацию кода, применять заданные в исходном тексте отступы, перенос строк.
Не забывайте создать CSS-файл и JavaScript файл и подключить их оба. Переходим к JavaScript.
Поиск не латинских символов с помощью строки "разрешенных символов"
Для начала в JS файле поставим константы, с которыми будем работать.
const innerText = document.querySelector('#latin-text');
const outerText = document.querySelector('.latin-result');
Теперь повесим на кнопку события клик, и напишем функцию, которая будет получать textarea text. После получения текст очищается от пробелов в начале и конце строки с помощью trim. Также добавляем проверку, если текст не ввели (пустая строка), исходим из функции.
const innerText = document.querySelector('#latin-text');
const outerText = document.querySelector('.latin-result');
document.querySelector('button').onclick = function() {
let text = innerText.value.trim();
if (text ==='') return;
}
Самый простой вариант найти символы не латинские - перечислить разрешенные символы и проверять по этому паттерну. Если взять латинский алфавит, то получим:
const template = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz';
все не входящие сюда символы не латинские. Но позвольте, ведь есть еще числа, знаки препинания, скобки и так далее. Добавим их. Обратите внимание, что при добавлении кавычек придется произвести их экранирование, с помощью \ обратного слеша. И сам слэш (и прямой и обратный) тоже добавляем. Получим:
const template = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz!@#$%
Теперь перейдем к алгоритму поиска латинских символов. Для начала мы создадим переменную out = '', куда будем накапливать ответ. Затем циклом проработаем исходный текст. Вот так:
let text = innerText.value.trim();
if (text ==='') return;
const template = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz!@#$%^&*()_+-={}[]?.,;:';
let out = '';
for (let i = 0; i <text.length; i++) {
}
Внутри цикла добавляем проверку. Если текущий символ text[i] не входит в строку template – значит символ не латинский и нам нужно его выделить. Для простоты будем выделять тегами <mark></mark>. Проверку организуем с помощью indexOf. Если символ входит в template, то просто добавляем его в out. Результат – выведем в outerText.
let text = innerText.value.trim();
if (text ==='') return;
const template = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz!@#$%^&*()_+-={}[]?.,;:';
let out = '';
for (let i = 0; i <text.length; i++) {
if(template.indexOf(text[i]) === -1) {
out += '<mark>'+text[i]+'</mark>';
}
else {
out+=text[i];
}
outerText.innerHTML = out;
}
Проверим, как код работает на исходных строках:
const out = document.querySelector('.out-1');
const out = document.querySelеctor('.оut-1');
Видно, что код нашел два не латинских символа, однако выдал странную проблему в месте, где слепил выходные строки. Попробуем сохранить переносы строк, как в исходном варианте. Если textarea вы производите перенос строки, то добавляемся символ \n новой строки. Нам, поскольку мы выводим на страницу HTML, необходимо произвести замену символа переноса строки на тег br. Перепишем условие if следующим образом:
for (let i = 0; i <text.length; i++) {
if (text[i] === 'n') {
out += '<br>';
}
else if(template.indexOf(text[i]) === -1) {
out += '<mark>'+text[i]+'</mark>';
}
else {
out+=text[i];
}
}
Тестуем на примере и видим, что результат выводится как и ожидалось и сохраняет форматирование. А символы кириллицы подсвечены правильно. Продолжим тестирование на других текстах. Напишите текст смеси латинских символов и других и все будет хорошо работать. Однако есть проблема. Давайте введем следующий код для проверки:
if (a<b) console.log('hi');
<b>Hello</b>

Визуально (см. изображение), все работает. Но если вывести в консоль, то увидим – удвоение угловых скобок, явно не валидный код.

Вы можете возразить, что мы в строке template не предполагали угловых скобок. Хорошо. Давайте допишем <> у template и проверим результат на той же строке. А результат окажется еще хуже.

С чем связано такое поведение? С тем, что текст с угловыми скобками трактуется как HTML-теги, так и браузер "отрабатывает" по ним. В последнем случае рекомендуется заменить символы "сущностями" , т.е. кодами, являющимися набором символов, но браузер трактует и выводит как знак, например, угловые скобки. Добавляем определение сущностей в проверку:
for (let i = 0; i <text.length; i++) {
if (text[i] === '>') {
out += '>';
}
else if (text[i] === '<') {
out += '<';
}
else if (text[i] === 'n') {
out += '<br>';
}
else if(template.indexOf(text[i]) === -1) {
out += '<mark>'+text[i]+'</mark>';
}
else {
out+=text[i];
}
}
При проверке – видим, что этот код сработал и корректно вывел угловые скобки и определяет не латиницу. И тогда финальный код выглядит так:
const innerText = document.querySelector('#latin-text');
const outerText = document.querySelector('.latin-result');
document.querySelector('button').onclick = function() {
let text = innerText.value.trim();
if (text ==='') return;
const template = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz!@#$%^&*()_+-={}[]?.,;:';
let out = '';
for (let i = 0; i <text.length; i++) {
if (text[i] === '>') {
out += '>';
}
else if (text[i] === '<') {
out += '<';
}
else if (text[i] === 'n') {
out += '<br>';
}
else if(template.indexOf(text[i]) === -1) {
out += '<mark>'+text[i]+'</mark>';
}
else {
out+=text[i];
}
}
console.log(out);
outerText.innerHTML = out;
}
Осталось оптимизировать и зачесать код. Но мы оставим это на читателе.
Ищем не латинские символы с помощью регулярных выражений в JavaScript
Если у вас в коде программы идет работа с циклами и if операторами, то подумайте – возможно, регулярки будут быстрее и проще? Или проще зрительно (но не всегда быстрее). Давайте попробуем найти не латинские символы с помощью регулярных выражений. В качестве основы применим ту же функцию:
document.querySelector('button').onclick = function() {
let text = innerText.value.trim();
if (text ==='') return;
const template = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz!@#$%^&*()_+-={}[]?.,;:\'\"
let out = '';
console.log(out);
outerText.innerHTML = out;
}
Заменим константу с перечислением всех разрешенных символов с помощью регулярного выражения. Нам нужны:
let regexp = /[^\w\s'",\\.:;?!@#$%^&*+-=_{}\[\ ]()]/gi
где:
- \w буквенный или цифровой символ или знак подчеркивания; буквы ограничены латиницей
- \s пробел символ
а дальше мы перечисляем все символы, которые разрешены: кавычки, запятые, точки, знаки препинания и скобки. Перед символами, зарезервированными синтаксисом регулярных выражений, ставим обратный слеш для экранирования.
Знак ^ в начале регулярного выражения означает, что мы ищем все, кроме перечисленных символов.
Далее нужно аналогично циклу выше, перебрать строку и если символ не входит в перечисленные – заменить. Для такой операции идеально подходит метод .replace для регулярных выражений. Как он работает? Принимает регулярное выражение и при обнаружении совпадения применяет к символу функцию. Поэтому допишем следующий код:
let result = text.replace(regexp, symbol => '' + symbol + '';
Сделаем такой код и проверим результат.
document.querySelector('button').onclick = function() {
let text = innerText.value.trim();
if (text ==='') return;
let regexp = /[^\w\s'",\\.:;?!@#$%^&*+-=_{}\[\]()]/gi
let result = text.replace(regexp, symbol => '' + symbol + '';
outerText.innerHTML=result;
console.log(result); // HTML и CSS
}

Работает. Но, как и в предыдущем примере, есть проблемы с переносом строк и с угловыми скобками. В принципе можно все сделать в одном replace, однако у нас учебный урок, поэтому пишем условия максимально понятно:
document.querySelector('button').onclick = function() {
let text = innerText.value.trim();
if (text ==='') return;
let regexp = /[^\w\s'",\\.:;?!@#$%^&*+-=_{}\[\]()<>]/gi
let result = text.replace(/\>/g, str => '>');
result = result.replace(/\ '<');
result = result.replace(/\n/g, str => '
');
result = result.replace(regexp, symbol => '' + symbol + '');
outerText.innerHTML=result;
console.log(result); // HTML и CSS
}
Что мы здесь делаем? Сначала заменяем угловые скобки сущностями, затем перенос строки заменяем на тег br. Затем – если символ не входит в regexp, то вращаем символ тегом mark. Обратите внимание, мы выполнили замену угловых скобок сущностями, а затем – добавляем перенос строк. Нам нужно теперь, чтобы регулярное выражение не реагировало на скобки. Поэтому добавляем их в regexp.
Теперь код сохраняет форматирование после проверки, а не латинские (кириллица) знаки выделяются тегом mark, и визуально заметны. Также если скопировать проверенный код и вставить в JavaScript, все будет работать.
Код не оптимизирован. Можно уменьшить количество применения метода replace, что ускорит работу.
Просмотреть видео с этого урока: