Как стать автором
Обновить

Текст без опечаток в документации и не только: внедряем CSpell

Уровень сложностиСредний
Время на прочтение26 мин
Количество просмотров2K

Меня зовут Саша, я технический писатель в команде документации Yandex Cloud. В свободное от работы время я занимаюсь другой работой — изучаю инструменты для документирования и экспериментирую с ними. Один из моих экспериментов был посвящен изучению и внедрению спеллчекера CSpell в мои процессы создания документации.

Статья будет полезна всем, кто хочет научиться проверять тексты в исходных файлах, будь то Markdown, YAML или комментарии в коде. Больше всего пользы из нее вынесут технические писатели и те, кто формирует процессы для команд технических писателей.

В статье приведен обзор возможностей спеллчекера CSpell, а также проанализированы сложности, с которыми лично я столкнулся при адаптации этого инструмента для работы с нашей документацией. Я не ставлю целью изложить абсолютно все возможности CSpell — для этого есть документация от авторов этого инструмента, я показываю свой опыт и даю рекомендации на его основе.

Большая часть обзора относится к проверке русскоязычных текстов, полноценную конфигурацию для английского языка я как минимум пока не создал, так как с англоязычными текстами проблем оказалось намного меньше, и я использую одну конфигурацию для обоих языков. В статье практически не затрагиваются технические аспекты внедрения CSpell в CI, так как я этим не занимался.

Хочу выразить благодарность Ольге Поляковой, Агнессе Буковской и Сергею Кардапольцеву, которые внесли свой вклад в вычитку этой статьи и тестирование приведенных конфигураций на всех операционных системах и в самых разных сценариях — от проверки обычной документации в Markdown до анализа текста заметок в Org-Mode.

Оглавление

  1. TL;DR

  2. Предыстория

  3. Область применения спеллчекеров

  4. Основные возможности CSpell

  5. Начало работы и возникшие проблемы

  6. Ограничение области поиска

  7. Проблема поиска словоформ

  8. Продвинутый уровень создания собственных словарей

  9. Локальное использование

  10. Интеграция с Git и совместное использование

  11. Рекомендации по использованию в CI

  12. Дополнительные рекомендации

TL;DR

Перед использованием имеет смысл ознакомиться с введением в документации CSpell.
Для использования нужно установить сам CSpell, а также словарь для русского языка (словарь для английского поставляется по умолчанию с CSpell). Предполагается, что Node.js и npm уже установлены. Есть также другие варианты установки, см. в документации: Installation.

Установка самого CSpell будет производиться глобально, а все словари будут устанавливаться в node_modules в директории проекта. :

npm install -g cspell@latest

Установка словаря для русского языка:

npm install @cspell/dict-ru_ru

При работе с Git нужно добавить директорию node_modules в файл .gitignore, чтобы они не попадали в репозиторий.

Глобальная установка словарей также возможна, но в этом конкретном сценарии рассматривается работа со словарем в рамках одного документационного проекта.

Файл с минимальной конфигурацией в случае локальной установки выглядит так:

{
    "version": "0.2",
    "language": "en,ru",
    "import": [
        "@cspell/dict-ru_ru/cspell-ext.json",
    ]
}

Я рекомендую разместить конфигурационный файл в корне репозитория. Можно разместить его в любом месте на своей машине, если проверять нужно более одного репозитория или есть иные требования к структуре файлов, но тогда нужно каждый раз указывать путь к файлу с помощью аргумента --config <path>. Также нужно учитывать наличие относительных путей в конфигурации, так как это важно для выбора проверяемых файлов.

После этого можно запустить CSpell из командной строки, указав в качестве аргумента адрес файла или директории (поддерживается синтаксис Glob). Я использую также флаг --show-suggestions, чтобы сразу понимать вероятные варианты замены:

cspell lint path/to/file.md —show-suggestions

Для проверки директорий:

cspell lint path/to/folder/** —show-suggestions

Найденные правильные, но неизвестные слова можно добавить в свой словарь, в качестве примера используется project-words.txt. После этого слова, добавленные в этот словарь, не будут считаться ошибками. Добавить словарь можно следующим образом:

{
    "version": "0.2",
    "language": "en,ru",
    "import": [
        "@cspell/dict-ru_ru/cspell-ext.json",
    ],
    "dictionaryDefinitions": [
        {
            "name": "project-words",
            "path": "project-words.txt",
            "addWords": true
        }
    ],
    "dictionaries": [
        "project-words"
    ]

}

Чтобы запретить использование какого-либо слова, в словаре нужно написать перед ним восклицательный знак: !функционал. Слова, помеченные восклицательным знаком, всегда будут считаться ошибками.

Пример структуры словаря как с разрешенными, так и с запрещенными словами:

инстанс
инстансам
инстансах
инстансов
инстансом
!функционал
!аггрегация

С такой конфигурацией я проработал много месяцев, проверяя тексты в Markdown-файлах. Она позволяет решить большую часть задач, возникающих при работе с текстами, но для работы в команде нужно нечто большее, о чем будет подробно рассказано дальше.

Предыстория

Я всерьез начал заниматься документацией в 2019 году и тогда же познакомился с линтером Vale. Это прекрасный инструмент, который позволяет проверять естественный язык. Проблема заключается в том, что значительная часть функциональности у него работает только для английского языка.

Но в начале 2023 года я перешел в команду Yandex Cloud, и моим основным языком стал русский. Мне было непривычно и некомфортно, а еще я лишился инструмента, на который привык полагаться. Беглое исследование показало, что достичь всего, что мне было нужно — задача не из простых. Поэтому я начал изучать альтернативные инструменты.

В конечном счете я остановился на CSpell для проверки орфографии. Это спеллчекер, который запускается из командной строки, принимающий на вход адрес файла или директории и возвращающий незнакомые ему слова.

Исходники нашей документации написаны на языке Markdown с флейвором YFM и располагаются на GitHub. Команда у нас довольно большая, а еще больше людей периодически приносят пул-реквесты с изменениями, так что в документацию попадают самые разные тексты, и в этих текстах бывают опечатки разной степени критичности.

Но опечатки сами по себе — еще не вся проблема. Документация должна быть грамотно написана, но в больших корпусах текстов также важно соблюсти единство терминологии. Например, если команда договорилась, что «промт» пишется с одной буквой «п», то это слово должно быть написано таким же образом даже в текстах от людей, которые никогда не слышали про командные договоренности.

Область применения спеллчекеров

Тут стоит остановиться и четко зафиксировать, на что способен спеллчекер из коробки, а чего от него ожидать не следует. Я решил сосредоточиться на работе на уровне слова, чтобы в моей документации не было орфографических ошибок, и чтобы мне нужно было реже обращаться к нашему глоссарию. Такая постановка была связана с ключевым ограничением спеллчекеров — они не умеют работать со словосочетаниями и предложениями, зато отдельные слова проверяют быстро и хорошо.

В ходе исследования, тестирования и внедрения я неоднократно приходил к мысли, что некоторые вещи было бы легче делать с помощью других инструментов — например того же Vale или LanguageTool, потому что они способны на большее, чем просто исправление опечаток. Но в итоге я все-таки решил довести до конца внедрение изначально выбранного инструмента, так как он позволял крайне дешево решить одну конкретную проблему.

Из всех спеллчекеров я выбрал именно CSPell потому, что он использует Node.js и npm-пакеты, что хорошо ложится на технологический стек нашей команды. Сам CSpell умеет проверять любые языки, которые поддерживает VS Code: поддерживает множество естественных языков, включая русский, а также применим для языков программирования и разметки, которые используются у нас — в первую очередь Markdown, YAML и JavaScript.

Помимо перечисленного выше, CSpell интегрируется с одним из самых популярных плагинов для проверки орфографии в редакторе VS Code, в котором я пишу документацию — Code Spell Checker. У них один автор — компания Street Side Software, и одна и та же конфигурация может быть использована как для проверок через CSpell в терминале, так и при визуальных проверках, когда ошибки подчеркиваются синей волнистой линией. Фактически Code Spell Checker является оберткой для движка CSpell, и если в проекте есть конфигурационный файл для CSpell в любом формате, то указанные в нем параметры будут применяться и при визуальной проверке.

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

  1. Спеллчекер должен находить опечатки и обеспечивать использование установленного написания терминов.

  2. Получившееся решение должно работать как с русским, так и с английским языком.

  3. Решение должно одинаково хорошо работать как в пайплайне CI, так и локально.

  4. Все желающие должны иметь возможность работать локально с одной и той же конфигурацией.

  5. Решение должно работать во внутреннем контуре, быть опенсорсным и бесплатным.

  6. Конфигурация должна быть простой и легко расширяемой.

  7. Результаты проверок не должны создавать дополнительной когнитивной нагрузки.

Для проверки на уровне словосочетания, предложения и выше у нас внедряется линтер на основе нейросетей, про который мой коллега и я рассказывали на недавно прошедшей конференции TECHWRITER DAYS 2. В этой статье не будет анализа взаимодействия этих двух инструментов, но это важная причина того, почему я сконцентрировался именно на проверке отдельных слов.

Основные возможности CSpell

Итак, CSpell работает на уровне слова. Но что такое слово в понимании авторов этого инструмента? Ответ на этот вопрос чуть менее очевиден, чем может показаться.

Слово — это в первую очередь последовательность букв алфавита и цифр, до и после которой есть пробелы. Длина такой последовательности по умолчанию составляет четыре символа и более, но это можно изменить с помощью настройки minWordLength.

Другие особенности:

  • По умолчанию, CSpell разделяет и проверяет по отдельности слова, которые написаны в camelCase, snake_case и пр.

  • Вырезаются сочетания символов, относящиеся к экранированию — \t и \n.

  • При проверках игнорируются любые символы и их сочетания, не являющиеся буквами или цифрами. Если какие-нибудь символы, например звездочки, примыкают к последовательности букв и цифр без пробела, то проверяться будут только буквы и цифры. Пример: если в документации будет последовательность accountnumber***hgkdf, то проверена будет только часть accountnumber.

Подробнее см. в документации: How it works.

Конфигурация

Первым делом я хочу упомянуть ключевую функциональность CSpell — словари. Все слова, которые CSpell не находит ни в одном из словарей, считаются ошибками.

Словари

Словари бывают двух видов:

  • Предварительно скомпилированные в формате Hunspell, их устанавливают как npm-пакеты. Это словари для естественных языков, а также глоссарий терминов из нескольких областей, связанных с программированием. Такие словари составляют как пользователи, так и разработчики CSpell, и выкладывают на GitHub.

  • Собственные, которые хранятся локально как txt-файлы. Собственные словари можно наполнить любыми словами, по одному на строку.

Словари подключаются следующим образом:

    "dictionaryDefinitions": [
        {
            "name": "project-words",
            "path": "project-words.txt",
            "addWords": true
        },
        {
            "name": "forbidden",
            "path": "dicts/forbidden.txt",
            "addWords": true
        }
    ],
    "dictionaries": [
        "project-words",
        "forbidden"
    ]

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

Пример словаря project-words.txt c разрешенными словами:

аудитный
бакет
бэкенд

Чтобы запретить использование какого либо слова, в словаре нужно пометить его восклицательным знаком: !функционал.

Пример forbidden.txt:

!функционал
!аггрегация

Подробнее о других способах запрета использования слов см. в документации: Forbidden Words.

Настройка языков и локалей

CSpell по умолчанию позволяет проверять файлы всех типов, которые поддерживает VS Code. В некоторых условиях нужно указывать соответствующие языкам значения в опции enableFiletypes, но чаще всего это нужно для кастомизации проверок под разные языки. Базовые проверки можно проводить без указания типов файлов.

Выбор языка, например русского или английского, осуществляется с помощью настройки language. Выбирая язык, пользователь указывает CSpell, какой словарь нужно использовать по умолчанию. В своих проектах я использую "language": "en,ru", так как базовый словарь — английский, и мне нужна проверка множества слов, которые встречаются и в русскоязычных текстах.

Также можно указать специфические настройки для разных языков или групп. Например, можно включить возможность проверять сложные слова, часто встречающиеся в названиях методов и классов (символ * означает, что настройка применяется ко всем языкам и локалям):

	"languageSettings": [
		 {
			  "languageId": "*",
			  "locale": "*",
			  "allowCompoundWords": true
		 }
	]

Область поиска

За то, какие файлы и их части будут проверяться, отвечают настройки ignorePaths, ignoreRegExpList и includeRegexpList:

  • ignorePaths указывает, какие файлы и директории не будут проверяться.

  • ignoreRegExpList указывает паттерн в виде регулярного выражения. Последовательности символов, соответствующие этому паттерну, будут игнорироваться.

  • includeRegExpList указывает паттерн в виде регулярного выражения. Если указать эту настройку, то будут проверяться только и исключительно последовательности символов, соответствующие этому паттерну. Чаще всего использование этой настройки мне кажется неоправданным, за исключением случаев проверки файлов с кодом, где проверке должны подвергаться только комментарии.

Подробно взаимодействие ignoreRegExpList и includeRegExpList, а также про паттерны, можно прочитать в документации CSpell.

Мой собственный вариант наполнения ignoreRegExpList приведен в одной из следующих секций.

Начало работы и возникшие проблемы

Итак, требования были собраны, ограничения изучены, и я собрал первую локальную конфигурацию для проверки своих гипотез.

Реализация была довольно наивной — я использовал словарь для русского языка, созданный на основе соответствующего словаря Hunspell, который, в свою очередь, скомпилирован на основе русскоязычного словаря из LibreOffice. Далее я планировал найти опечатки в наборе тестовых файлов и правильные, но незнакомые спеллчекеру слова, и добавить их в собственный словарь, а опечатки исправить.

Как примерно выглядела конфигурация:

{
    "version": "0.2",
    "language": "en,ru",
    "import": [
        "@cspell/dict-ru_ru/cspell-ext.json",
    ],
    "dictionaryDefinitions": [
        {
            "name": "project-words",
            "path": "dicts/project-words.txt",
            "addWords": true
        },
    ],
    "dictionaries": [
        "project-words",
    ],
   "languageSettings": [
		  {
		      "languageId": "*",
		      "locale": "*",
		      "caseSensitive": false
		  }
    ]
}

После этого я запустил проверку:

cspell lint ru/_test-folder/test-file.md

На синтетических примерах в тексте все прошло неплохо, но при проверке настоящей документации вывод оказался заполнен бессмысленными сочетаниями букв вперемешку со словами вроде «нереплицируемый». В директории с несколькими десятками файлов ориентироваться в выводе было уже затруднительно.

Изучив результаты, получившиеся при анализе нескольких разных разделов, я понял, что спеллчекер делал именно то, что он должен был делать — искал последовательности символов, которые считал словами. Но у нас в документации много плейсхолдеров, в которых маскируется часть символов, есть примеры ключей и токенов, названия библиотек и тому подобное. Они и обеспечили обилие нечитабельных символов, и их было намного больше, чем просто незнакомых слов и тем более реальных опечаток, я бы сказал, что только одно из ста найденных вхождений действительно было опечаткой.

Первой моей реакцией было желание добавить все такие фрагменты в специальный словарь, не предназначенный для чтения человеком. Но в таком случае мне пришлось бы совершить вручную очень много действий и повторять эти действия с каждым появлением нового плейсхолдера, или же мириться с присутствием такого визуального мусора в результатах проверок.

Чтобы избежать этого, я начал искать решение двух основных проблем: как проверять только настоящие слова, игнорируя бессмысленные сочетания символов, и как добавить в словарь как можно больше правильно написанных слов, не являющихся частью основного корпуса русского языка.

Ограничение области поиска

В нашем документационном проекте файлы достаточно жестко распределены по директориям. Верхний уровень — это язык (ru или en), на следующем уровне идет разделение по сервисам, а также на служебные директории.

С помощью настройки ignorePaths я исключил из проверок служебные файлы, саму конфигурацию CSpell, чтобы не создавать рекурсию, и некоторые специфические для нашего проекта файлы директории примерно следующим образом:

    "ignorePaths": [
        "node_modules",
        "cspell",
        "utils",
        "ru/presets.yaml",
        "en/presets.yaml"
    ]

Но такое точечное улучшение снимало только часть проблемы. Нужно было более комплексное решение для всех элементов, которые засоряют вывод спеллчекера. В моем случае это часто были элементы Diplodoc — переменные, блоки кода, вкладки, условные выражения, а также liquid-подобные шаблоны и якоря для ссылок, генерируемые VS Code. Чтобы игнорировать такие элементы при проверках, я создал шаблоны в виде регулярных выражений.

В CSpell они могут выглядеть так, с учетом экранирования символов, которое требует JSON:

    "ignoreRegExpList": [
		  "```[\\s\\S]*```",
		  "{{\\s*[\\s\\S]*?}}",
		  "{\\#[\\s\\S]*?}",
		  "{%\\s*[\\s\\S]*?%}",
		  "`[^\n]*`"
    ]

Вот что игнорируют эти регулярные выражения:

  • ```[\s\S]*```
    Все блоки кода, которые заключены в тройные обратные апострофы;

  • {{\s*[\s\S]*?}}
    Переменные YFM, которые заключены в фигурные скобки, например {{ variable.name }};

  • {\#[\s\S]*?}
    Все якоря, которые заключены в фигурные скобки, например {#quickstart};

  • {%\s*[\s\S]*?%}
    Все элементы YFM, которые заключены в фигурные скобки c символом процента, например условные выражения: {% if condition %};

  • `[^\n]*`
    Все символы, которые заключены в одинарные обратные апострофы, например `--name`, `--instance-ids epdl7gdvb69f********,epd3anghn4vb********`.

Таким образом удалось значительно повысить чистоту вывода при проверках без необходимости держать словарь токенов, сокращений, переменных и других элементов. Ценой такого улучшения стало сужение области действия проверок, то есть опечатка в комментарии, который расположен в многострочном блоке кода, так и останется не обнаруженной.

Это еще не был желаемый результат, но уже значительный прогресс. После этого я перешел к самой трудоемкой задаче, которая заняла у меня больше времени, чем все остальные части этого процесса.

Проблемы поиска словоформ

После первой стадии, когда я уже научился игнорировать элементы разметки и токены, я захотел собрать свой словарь, который мог бы закрыть разницу между основным корпусом русского языка и нашим глоссарием. Хочу предупредить, что это значительный объем работы, который не обязателен для использования спеллчекера, уже описанной выше конфигурации хватит для индивидуального использования. Но такое упражнение полезно для анализа существующей документации и очистки результатов проверок от ложных срабатываний.

Русскоязычная часть нашей документации включает в себя чуть более 12 000 файлов, не считая изображений и некоторых файлов, содержащих машиночитаемую информацию. Для начала я остановился на одной из директорий, в которой было около 300 файлов. После проверки я добавил в словарь такие слова как «инстанс», «нереплицируемых», «расшифрованием» и «переиспользуем». Если посмотреть на них внимательно, можно заметить, что только одно слово находится в начальной форме — «инстанс».

Когда я убедился, что в директории, которую я проверял, не осталось опечаток, я запустил проверку сразу нескольких директорий, содержавших в сумме примерно 1500 файлов. По результатам этой проверки я добавил в словарь «инстансы», «инстансов» и «переиспользовать», а также многие другие слова. Стало ясно, что чем больше файлов я буду проверять, тем чаще мне будут встречаться разные формы уже добавленных ранее слов, а еще при создании новых текстов в документации появятся ранее не встречавшиеся формы.

Для решения этой проблемы я решил сразу добавлять в словарь все возможные формы слова, как только мне встретится хоть одна из них. Для этого нужно было найти инструмент, который позволит модифицировать русские слова — склонять, спрягать, менять число и т.д.

В наши дни я рекомендую использовать любую доступную модель нейросети для обработки и трансформации слов. Модели можно отправить слово в любой форме и попросить привести его в начальную форму, а потом просклонять, проспрягать и провести все остальные необходимые трансформации. Ответ модели в большинстве случаев можно вставить в свой словарь.

Именно так я и поступил — изначально я использовал YandexGPT 5 Lite, потом перешел на плагин SourceCraft Code Assistant для VS Code. Как правило, одиночные слова не будут составлять коммерческую или иную тайну, но в таких делах консультация с юристами никогда не бывает лишней.
Фрагмент созданного таким образом словаря на GitHub.

Необязательное отступление про склонение и спряжение

До массового распространения тогда еще ChatGPT 3.5 я использовал другое решение — библиотеку pymorphy2, которая была представлена на Хабре в далеком 2013 году.
Я не буду останавливаться на устройстве этой прекрасной библиотеки, исследование которой обеспечило мне много интереснейших вечеров и ночей, а также на нюансах ее работы. Результатом моего изучения стала недописанная утилита командной строки, в которой я обрабатывал поступающие слова следующим образом: dcmn.

Почему я не буду на этом останавливаться? У библиотеки были свои ограничения — она зависит от инструментария OpenCorpora, и определенная часть нужных мне слов обрабатывалась некорректно. Не буду утверждать, что это вызвано исключительно спецификой работы выбранного инструмента — вероятно, я также допустил ошибки при обращении к библиотеке из своего кода. Я не планирую в будущем развивать эту утилиту, зато у меня остался забавный артефакт — результат обработки с помощью этой утилиты слов из русскоязычного Вики-словаря компьютерных терминов. Я исключил оттуда слова, которые ни в каком виде не могли использоваться в документации, а остальные просклонял и проспрягал с помощью ранней версии своей утилиты. Этот словарь я также не использую в работе.

Продвинутый уровень создания собственных словарей

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

CSpell, как и большинство спеллчекеров, совместим со словарями, которые использует Hunspell. Я уже приводил ссылку на пример такого словаря — CSpell Russian Dictionary, а на странице этого словаря на GitHub есть ссылка на оригинальный словарь для Hunspell. Если упростить, то Hunspell-совместимый словарь состоит из двух файлов:

  1. Dictionary file (.dic) — содержит список слов в начальной форме и флаги для указания аффиксов.

  2. Affix file (.aff) — содержит правила для обработки всех аффиксов, то есть отвечает за обработку флагов, что позволяет образовывать разные формы слов.

Подготовка такого словаря — большой труд, а после публикации его нужно обновлять, перекомпилировать и устанавливать на все машины, где работают с этим словарем. Этот процесс был описан в статье от компании Флант. Там также рассматривался иной подход к проверке текстов и использование спеллчекера Aspell, а не CSpell, но эта статья в любом случае стоит того, чтобы с ней ознакомиться.

Но для своих целей я посчитал такой «налог» на поддержку спеллчекера чрезмерным. Одним из требований у меня была максимально простая и легко расширяемая конфигурация, в которую может внести свой вклад человек с любым уровнем подготовки, так что я решил держать все словари в формате txt, хотя это, конечно, имело свои нюансы.

Создание и наполнение словарей

При анализе большого массива текстовых файлов стоит завести один словарь, в который будут сразу складываться все результаты. Так будет проще сначала собрать все слова, а затем распределить их по всем нужным словарям. Для этого воспользуйтесь следующим скриптом (предполагается запуск из терминала с поддержкой bash):

touch dictionaries/draft.txt

echo "# <service_1>” >> dictionaries/draft.txt
cspell --words-only --unique “service_1/**" | sort --ignore-case >> dictionaries/draft.txt

В этом скрипте происходит следующее:

  1. Сначала создается файл для словаря.

  2. В файл записывается название проверяемой директории, в этом примере — service_1. Будут проверены все файлы со всеми разрешенными расширениями. Это нужно, чтобы понимать, откуда появилось какое-то из слов.

  3. Запускается CSpell с флагами, которые показывают только обнаруженные слова и только в одном экземпляре.

  4. Слова сортируются в алфавитном порядке и записываются в файл draft.txt.

После окончания работы над первой директорией можно перейти к следующей и повторять процесс до получения желаемого результата:

echo "# <service_2>” >> dictionaries/draft.txt
cspell --words-only --unique “service_2/**” | sort --ignore-case >> dictionaries/draft.txt

В результате получится словарь, который содержит неизвестные слова и сочетания символов из всего документационного проекта. После этого можно распределять слова по словарям. Предлагается следующая структура:

|__docs-root   # корень репозитория
    |__src-docs   # директория, в которой могут храниться исходники документации
	 |__cspell.json   # конфигурационный файл CSpell   
	 |__ dicts   # директория для хранения словарей
		|__draft.txt   # словарь, в который слова складываются для последующего разбора
    	|__allowed-ru.txt   # разрешенные слова на русском во всех нужных числах, лицах и падежах
    	|__allowed-en.txt   # разрешенные слова на английском
    	|__internal.txt   # слова и сочетания символов, которые можно просто игнорировать
    	|__forbidden-ru.txt   # запрещенные слова на русском во всех нужных числах, лицах и падежах
    	|__forbidden-en.txt   # запрещенные слова на английском

Синтаксис в собственных словарях

Выше уже рассматривалась часть синтаксиса в собственных словарях — использование восклицательного знака для запрета определенных слов (!слово). Но это не единственный возможный способ разметки слов для различных целей.

В собственных словарях можно использовать различные префиксы и суффиксы, чтобы определить, в каких случаях может быть использовано слово. Например, можно указать, что слово может использоваться только в сочетании с другими словами. Я не нашел случаев, когда эти возможности могут принести ощутимую пользу, но рекомендую ознакомиться с ними в документации: Words List Syntax.

Локальное использование

На этой стадии в проекте уже есть конфигурационный файл в корне репозитория и набор словарей, в которых могут храниться разрешенные и запрещенные слова. Также уже должны быть установлены необходимые пакеты и определена область действия:

{
    "version": "0.2",
    "language": "en,ru",
    "dictionaryDefinitions": [
        {
            "name": "allowed-ru",
            "path": "dicts/allowed-ru.txt",
            "addWords": true
        },
        {
            "name": "allowed-en",
            "path": "dicts/allowed-en.txt",
            "addWords": true
        },
        {
            "name": "internal",
            "path": "dicts/internal.txt",
            "addWords": true
        },
        {
            "name": "forbidden-ru",
            "path": "dicts/forbidden-ru.txt",
            "addWords": true
        },
        {
            "name": "forbidden-en",
            "path": "dicts/forbidden-en.txt",
            "addWords": true
        },
    ],
    "dictionaries": [
        "allowed-ru",
        "allowed-en",
        "forbidden-ru",
        "forbidden-en",
        "internal"
    ],
    "import": [
        "@cspell/dict-ru_ru/cspell-ext.json",
    ],
    "ignorePaths": [
        "node_modules",
        "cspell",
        "utils",
		  "ru/presets.yaml",
		  "en/presets.yaml",
    ],
	 "languageSettings": [
		  {
		      "languageId": "*",
		      "locale": "*",
		      "allowCompoundWords": true
		  }
	 ],
    "ignoreRegExpList": [
		  "```[\\s\\S]*```",
		  "{{\\s*[\\s\\S]*?}}",
		  "{\\#[\\s\\S]*?}",
		  "{%\\s*[\\s\\S]*?%}",
		  "`[^\n]*`"
    ]
}

Теперь можно переходить к проверке:

cspell lint service_1/sample.md --show-suggestions

Результат может выглядеть примерно так:

1/1 service_1/sample.md 57.66ms X
service_1/sample.md:1:11 - Unknown word (Алерты) Suggestions: [Алеты, Алетты, Алеуты, Алберты, Алверты]
service_1/sample.md:1:30 - Unknown word (namee) Suggestions: [name, named, namer, names, nape]
service_1/sample.md:2:107 - Unknown word (cылку) Suggestions: [ссылку, ссылка, ссыпку, усылку, сыску]
service_1/sample.md:3:224 - Unknown word (отслеживани) Suggestions: [отслеживании, отслеживание, отслеживанию, отслеживания, отслеживаниие]
CSpell: Files checked: 1, Issues found: 4 in 1 file.

Поиск слов в словарях

При наполнении словарей может возникнуть ситуация, когда члены команды не могут сразу сказать, в каком словаре есть слово, и есть ли оно уже в каком-то словаре вообще. У меня это впервые произошло при появлении в логах проверок слова «Cоздайте». Я потратил некоторое количество времени, перед тем как понял, что первая буква в этом слове — латинская, и долго изучал словари, чтобы понять, в чем проблема.

Вместо этого я мог бы воспользоваться поиском по словарям с помощью команды cspell trace <word> чтобы убедиться, что этого слова нет ни в одном словаре.

Пример использования для слова «instance»:

cspell trace instance

Результат выполнения:

cspell trace instance   
Word    F Dictionary           Dictionary Location
instance * aws*                 node_modules/@cspell/dict-aws/dict/aws.txt
instance - coding-compound-ter* node_modules/@cspell/dict-software-terms/dict/coding-compound-terms.txt
instance - companies*           node_modules/@cspell/dict-companies/dict/companies.txt
instance - computing-acronyms*  node_modules/@cspell/dict-software-terms/dict/computing-acronyms.txt
instance * cpp                  node_modules/@cspell/dict-cpp/dict/cpp.txt
instance - cryptocurrencies*    node_modules/@cspell/dict-cryptocurrencies/dict/cryptocurrencies.txt
<...>

Интеграция с Git и совместное использование

При совместном использовании с Git можно просто поделиться конфигурацией словарей, добавив их в репозиторий. Затем каждый член команды, у которого есть копия этого репозитория, сможет поставить необходимые npm-пакеты. После этого все смогут проверять любые файлы локально.

Такие проверки удобны, но в реальных условиях не всегда есть время и желание каждый раз вспоминать, какие файлы были изменены в процессе работы, и проверки могут быть пропущены. Это можно исправить, если настроить использование прекоммитных проверок, или, как их называют, хуков (pre-commit hook).

Git-хуки — это скрипты, которые Git запускает автоматически при определенных событиях в процессе работы с репозиторием. Они позволяют настроить автоматизацию действий на разных этапах работы с Git. Хуки делятся на несколько групп в зависимости от стадии, на которой они срабатывают:

  • Хуки на стороне клиента

    • pre-commit — запускается перед созданием коммита;

    • commit-msg — запускается для проверки сообщения коммита;

    • post-commit — запускается после завершения коммита;

    • post-merge — запускается после merge;

    • pre-push — запускается перед отправкой изменений на удаленный сервер.

  • Хуки на стороне сервера

    • pre-receive — запускается при получении push;

    • update — аналогичен pre-receive, но для каждой ветки;

    • post-receive — запускается после успешного приема всех изменений.

Это далеко не полный список хуков, но он даёт представление о возможностях автоматизации.

Реализация прекоммитной проверки

В статье будет рассмотрено создание и использование одного из наиболее полезных хуков — pre-commit. В примерах для проверки работы хука используется консольный клиент Git, но хук также будет работать и при использовании графического клиента, такого как GitHub Desktop.

Этот хук выполняется автоматически перед созданием коммита, когда вы уже добавили файлы в индекс с помощью git add, но еще не закоммитили изменения — git commit. В предлагаемой реализации этот хук проанализирует файлы и, в случае нахождения ошибок, предотвратит создание коммита до исправления текста или изменения конфигурации CSpell.

  1. Чтобы настроить хук, создайте файл pre-commit в директории .git/hooks/. Эта директория есть в любом репозитории Git, хотя она визуально не отображается. В нее нужно перейти из командной строки из корня репозитория и создать файл с названием pre-commit без расширения:

    cd .git/hooks 
    touch pre-commit
    
  2. Запишите в файл команды для автоматического вызова CSpell:

    cat > pre-commit << 'EOF'
    #!/bin/bash
    
    exec git diff --cached --name-only | npx cspell --no-summary --no-progress --no-must-find-files --file-list stdin
    EOF
    
  3. Проверьте содержимое файла:

    cat pre-commit
    

    В консоли должен появиться следующий результат:

    #!/bin/bash
    
    exec git diff --cached --name-only | npx cspell --no-summary --no-progress --no-must-find-files --file-list stdin
    
  4. В этой же директории нужно сделать файл исполняемым:

    chmod +x pre-commit
    
  5. После этого можно вернуться в корень репозитория (на два уровня вверх):

     cd ../..
    
  6. На этой стадии хук уже готов к работе. Можно добавить ошибку в любой файл и попробовать закоммитить изменения.

    Сначала нужно убедиться, что Git обнаружил изменения:

    docs-repo git:(my-branch) ✗ git status
    On branch my-branch
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   service_1/sample.md
    
    no changes added to commit (use "git add" and/or "git commit -a")
    

    После этого можно попытаться закоммитить изменения:

    docs-repo git:(my-branch) ✗ git commit -am "test precommit"
    service_1/sample.md:3:1 - Unknown word (ашипка)
    

    В консоли видно, что CSpell обнаружил ошибку в файле service_1/sample.md и коммит не произошел. Теперь можно исправить ошибку и попробовать закоммитить изменения еще раз.

    Есть способ закоммитить изменения даже при проваленной прекоммитной проверке с помощью флага --no-verify. Рекомендуется использовать этот способ только с полным сознанием того, что вы делаете:

    git commit --no-verify -m "test precommit"
    

Чтобы перестать применять этот хук, нужно удалить файл pre-commit в директории .git/hooks/:

```bash
cd .git/hooks
rm pre-commit
```

В документации CSpell также приведен пример реализации прекоммитной проверки для сообщений в коммитах.

Рекомендации по совместному использованию

  • У всех, кто работает с репозиторием, установлены необходимые версии CSpell и пакеты со словарями.

  • Все пользуются одной и той же конфигурацией cspell.json, расположенной в репозитории.

  • Все достаточно часто обновляют ветки, чтобы обновления словарей попадали к ним для локального использования.

  • Есть простое и понятное руководство по расширению и обновлению словарей.

  • При использовании прекоммитной проверки у всех корректно установлены хуки.

Рекомендации по использованию в CI

В этой секции рассматривается концепция добавления проверки с помощью CSpell в качестве одного из шагов в пайплайне CI. В отличие от шагов в остальных секциях, я не занимался непосредственным внедрением в CI. Мое участие в реализации этого механизма ограничилось подготовкой прототипа. В качестве механизма был выбран GitHub Action.

Скрипт импортирует словарь для русского языка и использует тот же файл cspell.json, который используется для локальных проверок. При открытии пул-реквеста скрипт проверяет с помощью CSpell все измененные строки. При следующем коммите в ту же ветку он проверит только строки, добавленные в новом коммите.

Я не рекомендую использовать приведенный ниже скрипт в текущей реализации в продакшене, так как он будет запускать выполнение всех шагов каждый раз, когда в ветке будут появляться изменения.

Более правильным подходом является вызов скрипта по команде и проверка измененных строк в тех файлах, которые попадают в дельту между HEAD текущей ветки и HEAD целевой ветки. Также я рекомендую настроить в репозитории вебхук, который позволит выполнять проверки на серверах компании, а не на вычислительных мощностях GitHub, бесплатный лимит которых ограничен.

Чтобы настроить проверку в CI в этой реализации, добавьте в файл .github/workflows/cspell.yml следующий код:

name: CSpell Check

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  spellcheck:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install CSpell
        run: |
          npm install -g cspell
          npm install -g @cspell/dict-ru_ru
          cspell link add @cspell/dict-ru_ru
      - name: Fetch all modified files
        id: files
        run: |
          echo "::set-output name=all_files::$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | tr '\n' ' ')"
      - name: Run CSpell on all changed files
        continue-on-error: true
        run: |
          cspell --config cspell.json --show-suggestions ${{ steps.files.outputs.all_files }}

В конфигурационном файле может потребоваться указание опции globRoot. Она позволит точнее указывать относительный путь для файлов, так как на серверах, где происходит выполнение кода в CI, может быть иная файловая структура "globRoot": "${cwd}". cwd — текущая директория, в которой выполняется код.

Для оптимизации потребления ресурсов также можно настроить кеширование с помощью флага --cache. Подробнее см. в документации: Cache settings.

Дополнительные рекомендации

В этой статье рассматривалось использование CSpell для проверки орфографии в документации, написанной с помощью Markdown и YAML. Но CSpell может быть использован и для проверки орфографии в коде. Сценарий, в котором такие проверки могут быть полезны — проверка комментариев, которые используются для генерации документации.

Проверка текстов документации в других форматах

При работе с документацией, созданной с помощью других инструментов, подход к проверке практически не меняется. Основным отличием является иной перечень сочетаний символов, которые нужно исключать из проверок. Я сгенерировал несколько регулярных выражений для этой задачи, которые демонстрируют примеры целевых сочетаний. Эти регулярные выражения нужно использовать как часть ignoreRegExpList в конфигурационном файле.

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

  • Docusaurus

    <[a-zA-Z][^>]*?>[^<]*?<\/[a-zA-Z][^>]*?>         // JSX/HTML
    <[a-zA-Z][^/>]*?\/>                                                  // Self-closing JSX/HTML tags
    import\s+[^;]+?;                                                        // Import statements
    export\s+[^;]+?;                                                        // Export statements
    {[^}]*?}                                                                       // JS expressions in JSX
    \{@\w+[^}]*?}                                                            // MDX plugins/components
    \{%[^%]*?%}                                                             // Шаблоны
    
  • Sphinx+rST

    \.\.\s+code-block::[^\n]*\n(?:[^\n]*\n)*?(?:\s{4,}[^\n]*\n)+   // Code blocks with indentation
    `[^`]*`                                                              // Inline code with double backticks
    \.\.\s+[a-z-]+::[^\n]*                                        // Directives (like .. note::)
    \|[^|]+\|                                                              // Substitution references
    ::[^\n]*\n\s{4,}[^\n]*                                        // Literal blocks
    :[a-z-]+:`[^`]*`                                               // Roles (like :ref:, :doc:)
    \${[^}]*}                                                           // Template variables 
    

Проверка комментариев в коде

В отличие от проверки документации, проверка комментариев в коде требует более тонкой настройки. В документации проверяются файлы с текстом, из которых нужно исключить некоторые элементы. При работе с кодом нужно игнорировать большую часть содержимого файла, проверяя только некоторые фрагменты. В этом сценарии будет полезно использовать настройку includeRegExpList, которая поможет ограничить поиск исключительно до заданных паттернов.

Также стоит добавить предварительно скомпилированные словари для соответствующего языка программирования.Некоторые из таких словарей я показываю в следующем разделе.

Для иллюстрации этого подхода я сгенерировал конфигурационный файл для CSpell, который предполагает работу как с документацией в формате Markdown и YAML, так и с комментариями в коде на C++, предназначенными для обработки в Doxygen. У меня нет практического опыта проверок файлов с кодом, поэтому приведенная ниже конфигурация является только демонстрацией подхода.

Для полноценной работы конфигурации:

  1. Создайте собственные словари для C++ и Doxygen:

    • dicts/cpp-terms.txt может содержать специфические термины и идентификаторы, используемые в вашем C++ коде, например iostream, std, cout, cin, endl и т.д.

    • dicts/doxygen-terms.txt может содержать команды Doxygen, например param, return, brief и т.д.

  2. Установите предварительно скомпилированный словарь для C++ с помощью npm:

npm install --save-dev @cspell/dict-cpp

В файле cspell.json:

{
    "version": "0.2",
    "language": "en,ru",
    "dictionaryDefinitions": [
        {
            "name": "project-words",
            "path": "dicts/project-words.txt",
            "addWords": true
        },
        {
            "name": "cpp",
            "path": "dicts/cpp-terms.txt",
            "addWords": true
        },
        {
            "name": "doxygen-terms",
            "path": "dicts/doxygen-terms.txt",
            "addWords": true
        },
    ],
    "dictionaries": [
        "project-words",
    ],
    "import": [
        "@cspell/dict-ru_ru/cspell-ext.json",
        "@cspell/dict-software-terms/cspell-ext.json",
        "@cspell/dict-cpp/cspell-ext.json",
    ],
    "ignorePaths": [
        "node_modules",
        "cspell",
        "utils",
        "build",
        "bin",
        "lib",
        "*.o",
        "*.so",
        "*.a",
        "*.dll",
    ],
    "languageSettings": [
        {
            "languageId": "markdown, yaml",
            "locale": "*",
            "allowCompoundWords": false
        },
        {
            "languageId": "cpp",
            "allowCompoundWords": true,
            "dictionaries": [
                "cpp-terms",
	        "doxygen-terms"
             ],
            "patterns": [
                {
                    "name": "comments",
                    "pattern": "//.*|/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/|/\\*\\*.*\\*/"
                },
                {
                    "name": "strings",
                    "pattern": "\".*?\"|'.*?'"
                },
                {
                    "name": "doxygen-commands",
                    "pattern": "\\\\\\w+|@\\w+"
                }
            ]
        }
    ],
    "ignoreRegExpList": [
		  "```[\\s\\S]*```",
		  "`[^\n]*`"
        // Для C++ и Doxygen
        "#include\\s+[<\"][^>\"]*[>\"]",
        "#define\\s+\\w+(?:\\([^)]*\\))?",
        "template\\s*<[^>]*>",
        "namespace\\s+\\w+",
        "\\b[A-Z][A-Z0-9_]*\\b", // Константы в С++ в UPPER_CASE
        "\\\\\\w+\\{[^}]*\\}",           // Команды Doxygen с параметрами
        "@\\w+\\s+[^@\\*/]*",     // Команды Doxygen, начинающиеся с @
        "\\\\\\w+\\s+[^\\\\\\*/]*",  // Команды Doxygen, начинающиеся с \
        "(/\\*\\*|///)[^*]*?\\*/?"    // Doxygen блоки комментариев
    ]
}

Дополнительные словари

CSpell предоставляет обширный набор словарей для различных языков и технологий. Вы можете найти полный список в соответствующем репозитории на GitHub. Использование дополнительных словарей может быть полезным как для проверки документации, так и для снижения количества ложных срабатываний при проверке кода.

Некоторые из словарей, которые могут пригодиться для документации:

Языки программирования

Платформы и технологии

Общие

Использование других инструментов

В статье было показано, как использовать CSpell для проверки орфографии в документации в формате Markdown с добавлением YAML. CSpell — отличный инструмент со своими особенностями, но есть и другие инструменты, которые могут быть более подходящими для других ситуаций.

Например, мне все так же не хватает возможности использовать регулярные выражения для поиска словосочетаний, которая есть в Vale. При этом Vale поддерживает использование Hunspell-совместимых словарей для проверки орфографии, а как я показывал в предыдущих секциях, практически каждый словарь, используемый CSpell, основан на словаре Hunspell. Таким образом, Vale может открыть дополнительные возможности при использовании схожего инструментария.

Не во всех системах возможна установка npm-пакетов, и это может быть хорошим поводом использовать сам Hunspell. Это инструмент с долгой историей и обширным распространением, а описанные в данной статье подходы можно переиспользовать практически без изменений.

Также нельзя обойти вниманием такой инструмент как LanguageTool. Ему не нашлось места в этой статье, так как мой опыт работы с ним ограничился единичным экспериментом, в котором я использовал его опенсорсный форк libregrammar. Тем не менее, он вполне может использоваться для проверки как орфографии, так и грамматики, в том числе и для русского языка.

Заключение

Надеюсь, что эта статья окажется полезной. В будущем у меня наверняка появятся дополнения к тому, что было рассмотрено в статье. В таком случае я обязательно расскажу об этом в своем абсолютно не профессиональном канале в Telegram.

Спасибо за внимание!

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+18
Комментарии11

Публикации

Ближайшие события