БлогNot. Пишем "Добрый браузер правды" или замена слов на загружаемых веб-страницах

Пишем "Добрый браузер правды" или замена слов на загружаемых веб-страницах

Первым массово известным продуктом такого рода был "добрый браузер правды" от Навального и Ко, так что это не какая-то свежая аналогия :) Суть дела, думаю, понятна - как сделать так, чтобы на всех загружаемых страницах одни слова могли заменяться на другие в соответствии с неким словарём или списком. При этом хотелось бы, чтоб замены были достаточно гибкими, например, умели учитывать падежные конструкции русского языка и РеГиСтР символов.

Попробуем разработать такое приложение, а точней, расширение для "Хрома", "с нуля".

Файл манифеста manifest.json будет совсем типовым:

{
"manifest_version": 2,
"name": "Добрый браузер правды",
"version": "0.1",
"icons": { 
 "16": "icon32.png",
 "48": "icon128.png",
 "128": "icon128.png" 
},
"description": "Заменяет запрещённые слова на их синонимы",
"content_scripts": [
 {
  "matches": ["*://*/*"],
  "js": ["script.js"],
  "run_at": "document_end"
 }
]
}

В нём дополнительно указано, что замены делаются до конца документа (document_end). Кодировка этого и остальных файлов - Юникод (UTF-8).

Картинки icon32.png и icon128.png соответствующих размеров я нарисовал в Paint, приводить их здесь незачем, а главный файл script.js мы сейчас напишем.

Функцию для рекурсивного обхода всех узлов документа я банально скоммуниздил вот отсюда:

function walk(node) {
 var child, next;
 if (node.nodeType) switch (node.nodeType)  {
  case 1:  // Element
  case 9:  // Document
  case 11: // Document fragment
   child = node.firstChild;
   while ( child ) {
    next = child.nextSibling;
    walk(child);
    child = next;
   }
   break;
  case 3: // Text node
   replaceMe(node);
   break;
 }
}

Достаточно единственного глобального вызова этой функции, чтобы обход заработал:

walk(document.body);

Будет не так скучно, если некоторые замены мы сделаем вариативными, то есть, слово для замены будет случайно выбираться из списка вариантов. Поможет в этом маленькая функция randomWord:

function randomWord(words) {
 return words[Math.floor(Math.random()*words.length)];
}

Осталось написать функцию replaceMe, которая выполнит всю работу. Она будет устроена так:

function replaceMe(textNode) {
 var text = textNode.nodeValue;

 //Нужные замены в тексте text

 textNode.nodeValue = text;
}

Сами замены сделаем с помощью метода Javascript replace, который "понимает" не только тупую замену слова на слово, но может работать и с регулярными выражениями, обеспечивающими должную гибкость поиска словоформ.

Если Вы знакомы с RegExp'ами, то будет совсем легко, если нет - помогут показанные ниже простые примеры.

1. Просто меняем слово на слово, второе слово может иметь несколько вариантов

text = text.replace(/Путин/g, randomWord(["Обещалкин","Поребриков"]));

Здесь слова для замены склоняются так же, как искомое, учёт падежей излишен. Ключ /g после строки поиска нужно указывать обязательно, если требуются замены всех вхождений. Если к ключу добавить ещё символ i - будет игнорироваться регистр символов при поиске (/gi)

2. Замена целых словосочетаний

text = text.replace(/(Един)(ая|ой)(\s)(Росси)(я|и)/g, "Пархат$2$3Парти$5");
                   //  $1    $2    $3   $4     $5

$1, ..., $5 - номера вхождений подшаблонов в круглых скобках, эти подшаблоны можно ставить потом в строку для замены. Мы берём из исходной строки окончание первого слова $2, пробельные разделители $3 и окончание второго слова $5, так что получается всё корректно;

(ая|ой) - альтернатива; конструкцию (...|...) можно вкладывать произвольное количество раз. В ней легко учесть отличающиеся части слов;

(\s) - пробельные символы.

3. Применяем подшаблоны и замену по списку одновременно

text = text.replace(/(Медведев)(|а|е|у)/g, randomWord(["Айфончик$2","Нанопрезик$2"]));

Обратите внимание, что второй подшаблон может быть и пустым - (|а|е|у), это обеспечит нормальную замену для именительного падежа.

Нужно следить, если какие-то падежные окончания у слов для замены не совпадают с исходным, такие варианты можно обработать отдельно:

text = text.replace(/Медведевым/g, randomWord(["Айфончиком","Нанопрезиком"]));

4. Замена только в начале слова

text = text.replace(/(\s|^)(думск)(ий|ого|им|ом)/gi, "$1Шпанск$3");

^ - контроль начала слова. Слово "думский" будет обработано, а, к примеру, фамилия "Стародумский" - нет.

Здесь первая буква "Шпанского комитета" принудительно станет большой.

Если хотим, чтоб замены оставляли регистр первой буквы искомого слова "как есть", придётся часть выражений дублировать:

text = text.replace(/(\s|^)(Думск)(ий|ого|им|ом)/g, "$1Шпанск$3");
text = text.replace(/(\s|^)(думск)(ий|ого|им|ом)/g, "$1шпанск$3");

Это относится и к остальным пунктам.

Стоит сказать, что конструкция \b, использующаяся для обозначения границы слова, не работает с кириллицей (или работает как-то криво), хотя границу слова, написанного латинскими буквами, определяет. Может пригодиться также спецсимвол $, обозначающий конец слова.

Пополнив списки по этим несложным шаблонам, вы сможете сделать собственный комфортный для вас лексикон своему браузеру :)

А я упакую папку с исходниками расширения в ZIP и прикреплю его к статье, о подключении сторонних расширений к "Хрому" говорится здесь, о дурацком предупреждении на сторонние расширения - тут. После установки расширения проверить его работу можно, например, на этой странице.

 Скачать архив .zip с этим расширением Chrome (15 Кб)

P.S. из 2021-го. К сожалению, в актуальных версиях Firefox скачанное с этой страницы и установленное из файла расширение непосредственно работать уже не будет из-за изменения требований к служебным файлам. Хотя, наверное, несложно было бы модифицировать.

Проверил также удалённо в "Хромом" актуальной версии 88 - всё работает (скачать архив, распаковать без создания папки любым архиватором, в браузере обратиться Настройки - Расширения, разрешить "режим разработчика", потом "Загрузить распакованное расширение", показав на папку).

16.11.2015, 15:02 [7071 просмотр]


теги: язык javascript chrome

К этой статье пока нет комментариев, Ваш будет первым