@m4reve/eslint-plugin-ru-preposition-validator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ Все важные изменения в проекте будут документироваться в этом файле.
4
+
5
+ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
6
+ и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
7
+
8
+ ## [1.0.0] - 2026-01-24
9
+
10
+ ### Добавлено
11
+ - Первый релиз плагина
12
+ - Правило `no-hanging-prepositions` для проверки висящих предлогов
13
+ - Поддержка 70+ русских предлогов и частиц
14
+ - Автофикс с оборачиванием в JSX expression
15
+ - Поддержка специальных символов (№, §)
16
+ - Поддержка организационных форм (ООО, АО, ПАО, ЗАО, ОАО)
17
+ - Регистронезависимая проверка предлогов
18
+ - Полное покрытие тестами
19
+ - Документация и примеры использования
20
+ - Рекомендованная конфигурация ESLint
21
+
22
+ ### Технические детали
23
+ - Минимальная версия ESLint: 7.0.0
24
+ - Минимальная версия Node.js: 12.0.0
25
+ - Поддержка JSX текстовых узлов
26
+ - Использование неразрывного пробела Unicode (\u00A0)
@@ -0,0 +1,135 @@
1
+ # Руководство по внесению вклада
2
+
3
+ Спасибо за интерес к проекту! Мы рады любому вкладу.
4
+
5
+ ## Как внести вклад
6
+
7
+ ### Сообщить о баге
8
+
9
+ 1. Проверьте, не была ли проблема уже сообщена в [Issues](https://github.com/your-repo/eslint-plugin-ru-typography/issues)
10
+ 2. Создайте новый issue с описанием:
11
+ - Шаги для воспроизведения
12
+ - Ожидаемое поведение
13
+ - Фактическое поведение
14
+ - Версия Node.js, ESLint и плагина
15
+ - Пример кода, который вызывает проблему
16
+
17
+ ### Предложить новую функцию
18
+
19
+ 1. Создайте issue с меткой "enhancement"
20
+ 2. Опишите:
21
+ - Какую проблему решает предложение
22
+ - Как должна работать функция
23
+ - Примеры использования
24
+
25
+ ### Отправить Pull Request
26
+
27
+ 1. Форкните репозиторий
28
+ 2. Создайте ветку для вашей функции (`git checkout -b feature/amazing-feature`)
29
+ 3. Внесите изменения
30
+ 4. Убедитесь, что код соответствует стандартам:
31
+ ```bash
32
+ npm test
33
+ ```
34
+ 5. Закоммитьте изменения (`git commit -m 'Add amazing feature'`)
35
+ 6. Отправьте ветку (`git push origin feature/amazing-feature`)
36
+ 7. Откройте Pull Request
37
+
38
+ ## Разработка
39
+
40
+ ### Установка зависимостей
41
+
42
+ ```bash
43
+ npm install
44
+ ```
45
+
46
+ ### Запуск тестов
47
+
48
+ ```bash
49
+ npm test
50
+ ```
51
+
52
+ ### Структура проекта
53
+
54
+ ```
55
+ lib/
56
+ ├── index.js # Главный файл плагина
57
+ └── rules/
58
+ └── no-hanging-prepositions.js # Правило проверки предлогов
59
+
60
+ tests/
61
+ └── rules/
62
+ └── no-hanging-prepositions.test.js
63
+
64
+ examples/ # Примеры использования
65
+ ├── before.jsx # Код до исправления
66
+ └── after.jsx # Код после автофикса
67
+ ```
68
+
69
+ ### Добавление нового правила
70
+
71
+ 1. Создайте файл правила в `lib/rules/`
72
+ 2. Добавьте правило в `lib/index.js`
73
+ 3. Создайте тесты в `tests/rules/`
74
+ 4. Обновите README.md
75
+ 5. Обновите CHANGELOG.md
76
+
77
+ ### Написание тестов
78
+
79
+ Используйте `RuleTester` из ESLint:
80
+
81
+ ```javascript
82
+ const { RuleTester } = require('eslint');
83
+ const rule = require('../../lib/rules/your-rule');
84
+
85
+ const ruleTester = new RuleTester({
86
+ parserOptions: {
87
+ ecmaVersion: 2018,
88
+ sourceType: 'module',
89
+ ecmaFeatures: { jsx: true },
90
+ },
91
+ });
92
+
93
+ ruleTester.run('your-rule', rule, {
94
+ valid: [
95
+ // Корректные примеры
96
+ ],
97
+ invalid: [
98
+ // Некорректные примеры с ожидаемыми ошибками
99
+ ],
100
+ });
101
+ ```
102
+
103
+ ### Стиль кода
104
+
105
+ - Используйте 2 пробела для отступов
106
+ - Добавляйте JSDoc комментарии для функций
107
+ - Пишите понятные имена переменных
108
+ - Следуйте принципам чистого кода
109
+
110
+ ### Добавление предлогов или частиц
111
+
112
+ Если вы хотите добавить новые предлоги или частицы в списки:
113
+
114
+ 1. Откройте [lib/rules/no-hanging-prepositions.js](lib/rules/no-hanging-prepositions.js)
115
+ 2. Добавьте слово в соответствующий массив:
116
+ - `PREPOSITIONS_WITH_NBSP_AFTER` - для предлогов (пробел после)
117
+ - `PARTICLES_WITH_NBSP_BEFORE` - для частиц (пробел перед)
118
+ 3. Добавьте тесты с новым словом
119
+ 4. Обновите README.md
120
+ 5. Добавьте запись в CHANGELOG.md
121
+
122
+ ## Код поведения
123
+
124
+ - Будьте уважительны к другим участникам
125
+ - Конструктивная критика приветствуется
126
+ - Помогайте новичкам
127
+ - Сосредоточьтесь на коде, а не на людях
128
+
129
+ ## Лицензия
130
+
131
+ Внося вклад в проект, вы соглашаетесь на лицензирование вашего вклада под лицензией MIT.
132
+
133
+ ## Вопросы?
134
+
135
+ Если у вас есть вопросы, создайте issue с меткой "question".
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ESLint Plugin RU Typography
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @m4reve/eslint-plugin-ru-preposition-validator
2
+
3
+ ESLint плагин для проверки русской типографики в JSX коде. Предотвращает "висящие" предлоги путём автоматической вставки неразрывных пробелов.
4
+
5
+ ## Возможности
6
+
7
+ - ✅ Проверяет предлоги и частицы в русском тексте внутри JSX
8
+ - ✅ Автоматически заменяет обычные пробелы на неразрывные (`\u00A0`)
9
+ - ✅ Оборачивает исправленный текст в JSX expression `{\`text\`}`
10
+ - ✅ Поддерживает более 70 русских предлогов и частиц
11
+ - ✅ Работает с организационными формами (ООО, АО, ПАО)
12
+ - ✅ Обрабатывает специальные символы (№, §)
13
+
14
+ ## Установка
15
+
16
+ ```bash
17
+ npm install @m4reve/eslint-plugin-ru-preposition-validator --save-dev
18
+ ```
19
+
20
+ или
21
+
22
+ ```bash
23
+ yarn add -D @m4reve/eslint-plugin-ru-preposition-validator
24
+ ```
25
+
26
+ ## Использование
27
+
28
+ ### Настройка ESLint
29
+
30
+ Добавьте плагин в секцию plugins вашего `.eslintrc`:
31
+
32
+ ```json
33
+ {
34
+ "plugins": ["@m4reve/ru-preposition-validator"],
35
+ "rules": {
36
+ "@m4reve/ru-preposition-validator/no-hanging-prepositions": "warn"
37
+ }
38
+ }
39
+ ```
40
+
41
+ Или используйте рекомендованную конфигурацию:
42
+
43
+ ```json
44
+ {
45
+ "extends": ["plugin:@m4reve/ru-preposition-validator/recommended"]
46
+ }
47
+ ```
48
+
49
+ ### Flat Config (ESLint 9+)
50
+
51
+ ```js
52
+ import ruPrepositionValidator from '@m4reve/eslint-plugin-ru-preposition-validator';
53
+
54
+ export default [
55
+ {
56
+ plugins: {
57
+ '@m4reve/ru-preposition-validator': ruPrepositionValidator
58
+ },
59
+ rules: {
60
+ '@m4reve/ru-preposition-validator/no-hanging-prepositions': 'warn'
61
+ }
62
+ }
63
+ ];
64
+ ```
65
+
66
+ ## Правило: no-hanging-prepositions
67
+
68
+ Это правило проверяет JSX текст на наличие "висящих" предлогов и частиц в русском языке.
69
+
70
+ ### Что проверяется
71
+
72
+ #### Предлоги (должен быть неразрывный пробел ПОСЛЕ)
73
+
74
+ ```
75
+ а, б, без, безо, будто, бы, в, во, ведь, вне, вот, всё, где, да, даже,
76
+ для, до, если, есть, ещё, же, за, и, из, изо, из-за, из-под, или, иль,
77
+ к, ко, как, ли, либо, между, на, над, надо, не, ни, но, о, об, обо,
78
+ около, оно, от, ото, перед, по, по-за, по-над, под, подо, после, при,
79
+ про, ради, с, со, сквозь, так, также, там, тем, то, тогда, того, тоже,
80
+ у, хоть, хотя, чего, через, что, чтобы, это
81
+ ```
82
+
83
+ #### Специальные символы и аббревиатуры
84
+
85
+ ```
86
+ №, §, АО, ОАО, ЗАО, ООО, ПАО
87
+ ```
88
+
89
+ #### Частицы (должен быть неразрывный пробел ПЕРЕД)
90
+
91
+ ```
92
+ б, бы, ж, же, ли, ль
93
+ ```
94
+
95
+ ### Примеры
96
+
97
+ #### ❌ Неправильно
98
+
99
+ ```jsx
100
+ // Обычный пробел после предлога
101
+ <div>Текст в коробке</div>
102
+ <p>Работа с данными</p>
103
+ <span>Дом № 5</span>
104
+
105
+ // Обычный пробел перед частицей
106
+ <div>Пойдём же</div>
107
+ <p>Правда ли это</p>
108
+ ```
109
+
110
+ #### ✅ Правильно (после автофикса)
111
+
112
+ ```jsx
113
+ // Неразрывный пробел вставлен, текст обернут в JSX expression
114
+ <div>{`Текст в\u00A0коробке`}</div>
115
+ <p>{`Работа с\u00A0данными`}</p>
116
+ <span>{`Дом №\u00A05`}</span>
117
+
118
+ <div>{`Пойдём\u00A0же`}</div>
119
+ <p>{`Правда\u00A0ли это`}</p>
120
+ ```
121
+
122
+ ### Автофикс
123
+
124
+ Правило поддерживает автоматическое исправление через `--fix`:
125
+
126
+ ```bash
127
+ eslint --fix src/
128
+ ```
129
+
130
+ При автофиксе:
131
+ 1. Обычные пробелы заменяются на **видимый литерал** `\u00A0` (unicode escape sequence)
132
+ 2. Весь текст оборачивается в JSX expression с template literal: `{\`text\`}`
133
+
134
+ **Важно**: В код вставляется видимая последовательность `\u00A0`, а не невидимый символ неразрывного пробела. Это обеспечивает читаемость кода и явное указание на использование неразрывного пробела.
135
+
136
+ ## Как это работает
137
+
138
+ ### До исправления
139
+
140
+ ```jsx
141
+ <div>Он пошёл в магазин за хлебом</div>
142
+ ```
143
+
144
+ ### После исправления
145
+
146
+ ```jsx
147
+ <div>{`Он пошёл в\u00A0магазин за\u00A0хлебом`}</div>
148
+ ```
149
+
150
+ В браузере это будет отображаться как:
151
+ ```
152
+ Он пошёл в магазин за хлебом
153
+ ```
154
+
155
+ Но предлоги "в" и "за" не смогут оторваться от следующих слов при переносе строки.
156
+
157
+ ## Запуск тестов
158
+
159
+ ```bash
160
+ npm test
161
+ ```
162
+
163
+ Тесты используют Jest и ESLint RuleTester.
164
+
165
+ ## Структура проекта
166
+
167
+ ```
168
+ eslint-plugin-ru-typography/
169
+ ├── lib/
170
+ │ ├── index.js # Экспорт плагина
171
+ │ └── rules/
172
+ │ └── no-hanging-prepositions.js # Правило проверки предлогов
173
+ ├── tests/
174
+ │ └── rules/
175
+ │ └── no-hanging-prepositions.test.js
176
+ ├── package.json
177
+ ├── jest.config.js
178
+ └── README.md
179
+ ```
180
+
181
+ ## Примеры использования в реальных проектах
182
+
183
+ ### React приложение
184
+
185
+ ```jsx
186
+ function Article() {
187
+ return (
188
+ <article>
189
+ <h1>{`Статья о\u00A0типографике`}</h1>
190
+ <p>{`Мы работаем с\u00A0текстом каждый день`}</p>
191
+ <p>{`Правда\u00A0ли это важно?`}</p>
192
+ </article>
193
+ );
194
+ }
195
+ ```
196
+
197
+ ### Next.js страница
198
+
199
+ ```jsx
200
+ export default function HomePage() {
201
+ return (
202
+ <div>
203
+ <h1>{`Добро пожаловать на\u00A0сайт`}</h1>
204
+ <p>{`ООО\u00A0"Компания"`}</p>
205
+ <p>{`Адрес: ул. Ленина, дом №\u00A027`}</p>
206
+ </div>
207
+ );
208
+ }
209
+ ```
210
+
211
+ ## Производительность
212
+
213
+ Плагин работает только с JSX узлами типа `JSXText`, что минимизирует влияние на производительность линтинга.
214
+
215
+ ## Совместимость
216
+
217
+ - ESLint: >= 7.0.0
218
+ - Node.js: >= 12.0.0
219
+ - React/JSX: все версии
220
+
221
+ ## Лицензия
222
+
223
+ MIT
224
+
225
+ ## Вклад в проект
226
+
227
+ Приветствуются pull requests и issue! Если вы нашли баг или хотите предложить улучшение, пожалуйста:
228
+
229
+ 1. Создайте issue с описанием проблемы
230
+ 2. Форкните репозиторий
231
+ 3. Создайте ветку для вашей фичи
232
+ 4. Напишите тесты
233
+ 5. Отправьте pull request
234
+
235
+ ## Авторы
236
+
237
+ Dmitriy Meshcheryakov
238
+
239
+ ## Благодарности
240
+
241
+ Плагин следует best practices из [Vercel React Best Practices](https://github.com/vercel/react-best-practices) для оптимальной производительности и качества кода.
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Пример ПРАВИЛЬНОГО кода после автофикса ESLint
5
+ * Все предлоги и частицы имеют неразрывные пробелы
6
+ */
7
+
8
+ function ArticleAfter() {
9
+ return (
10
+ <article className="article">
11
+ {/* Предлог "о" с неразрывным пробелом */}
12
+ <h1>{`Статья о\u00A0типографике`}</h1>
13
+
14
+ {/* Предлоги "с" и "в" с неразрывными пробелами */}
15
+ <p>{`Мы работаем с\u00A0текстом каждый день в\u00A0нашей жизни.`}</p>
16
+
17
+ {/* Частица "ли" с неразрывным пробелом перед ней */}
18
+ <p>{`Правда\u00A0ли это так важно?`}</p>
19
+
20
+ {/* Множественные предлоги с неразрывными пробелами */}
21
+ <p>{`Он пошёл в\u00A0магазин за\u00A0хлебом и\u00A0молоком.`}</p>
22
+
23
+ {/* Частица "же" с неразрывным пробелом */}
24
+ <p>{`Давайте\u00A0же начнём!`}</p>
25
+
26
+ {/* Частица "бы" с неразрывным пробелом */}
27
+ <p>{`Я\u00A0бы хотел узнать больше о\u00A0типографике.`}</p>
28
+
29
+ {/* Специальные символы с неразрывными пробелами */}
30
+ <address>
31
+ {`ООО\u00A0"Компания"`}<br />
32
+ {`ул. Ленина, дом №\u00A027, офис №\u00A05`}
33
+ </address>
34
+
35
+ {/* Организационные формы с неразрывными пробелами */}
36
+ <p>{`ПАО\u00A0"Газпром" и\u00A0АО\u00A0"Лукойл"`}</p>
37
+
38
+ {/* Сложный случай - все предлоги и частицы исправлены */}
39
+ <p>{`Если\u00A0бы я\u00A0знал о\u00A0том, что это так важно, я\u00A0бы начал раньше.`}</p>
40
+ </article>
41
+ );
42
+ }
43
+
44
+ export default ArticleAfter;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Пример НЕПРАВИЛЬНОГО кода - с висящими предлогами
5
+ * ESLint выдаст предупреждения для всех этих случаев
6
+ */
7
+
8
+ function ArticleBefore() {
9
+ return (
10
+ <article className="article">
11
+ {/* Предлог "о" должен иметь nbsp после него */}
12
+ <h1>Статья о типографике</h1>
13
+
14
+ {/* Предлоги "с" и "в" нуждаются в nbsp */}
15
+ <p>Мы работаем с текстом каждый день в нашей жизни.</p>
16
+
17
+ {/* Частица "ли" нуждается в nbsp перед ней */}
18
+ <p>Правда ли это так важно?</p>
19
+
20
+ {/* Множественные предлоги */}
21
+ <p>Он пошёл в магазин за хлебом и молоком.</p>
22
+
23
+ {/* Частица "же" */}
24
+ <p>Давайте же начнём!</p>
25
+
26
+ {/* Частица "бы" */}
27
+ <p>Я бы хотел узнать больше о типографике.</p>
28
+
29
+ {/* Специальные символы */}
30
+ <address>
31
+ ООО "Компания"<br />
32
+ ул. Ленина, дом № 27, офис № 5
33
+ </address>
34
+
35
+ {/* Организационные формы */}
36
+ <p>ПАО "Газпром" и АО "Лукойл"</p>
37
+
38
+ {/* Сложный случай с несколькими предлогами и частицами */}
39
+ <p>Если бы я знал о том, что это так важно, я бы начал раньше.</p>
40
+ </article>
41
+ );
42
+ }
43
+
44
+ export default ArticleBefore;
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ESLint plugin for Russian typography rules
3
+ * Prevents hanging prepositions by inserting non-breaking spaces
4
+ */
5
+
6
+ const noHangingPrepositions = require('./rules/no-hanging-prepositions');
7
+
8
+ module.exports = {
9
+ rules: {
10
+ 'no-hanging-prepositions': noHangingPrepositions,
11
+ },
12
+ configs: {
13
+ recommended: {
14
+ plugins: ['ru-typography'],
15
+ rules: {
16
+ 'ru-typography/no-hanging-prepositions': 'warn',
17
+ },
18
+ },
19
+ },
20
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @fileoverview Prevents hanging prepositions in Russian text by enforcing non-breaking spaces
3
+ * @author ESLint Plugin RU Typography
4
+ */
5
+
6
+ const PREPOSITIONS_WITH_NBSP_AFTER = [
7
+ "а", "без", "безо", "будто", "в", "во", "ведь", "вне", "вот",
8
+ "всё", "где", "да", "даже", "для", "до", "если", "есть", "ещё", "за",
9
+ "и", "из", "изо", "из-за", "из-под", "или", "иль", "к", "ко", "как",
10
+ "либо", "между", "на", "над", "надо", "не", "ни", "но", "о", "об", "обо",
11
+ "около", "оно", "от", "ото", "перед", "по", "по-за", "по-над", "под", "подо",
12
+ "после", "при", "про", "ради", "с", "со", "сквозь", "так", "также", "там",
13
+ "тем", "то", "тогда", "того", "тоже", "у", "хоть", "хотя", "чего", "через",
14
+ "что", "чтобы", "это", "№", "§", "АО", "ОАО", "ЗАО", "ООО", "ПАО",
15
+ ];
16
+
17
+ const PARTICLES_WITH_NBSP_BEFORE = ["б", "бы", "ж", "же", "ли", "ль"];
18
+
19
+ /**
20
+ * Escapes special regex characters in a string
21
+ */
22
+ function escapeRegex(str) {
23
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
24
+ }
25
+
26
+ /**
27
+ * Creates a regex pattern for prepositions that need nbsp after them
28
+ */
29
+ function createPrepositionAfterPattern() {
30
+ const escaped = PREPOSITIONS_WITH_NBSP_AFTER.map(escapeRegex);
31
+ // Match preposition followed by a regular space (not nbsp)
32
+ // Note: \b doesn't work with Cyrillic, so we use (?:^| ) for word start
33
+ // Note: \s includes nbsp (U+00A0), so we explicitly use space character
34
+ return new RegExp(
35
+ `(?:^| )(${escaped.join('|')}) +`,
36
+ 'gi'
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Creates a regex pattern for particles that need nbsp before them
42
+ */
43
+ function createParticleBeforePattern() {
44
+ const escaped = PARTICLES_WITH_NBSP_BEFORE.map(escapeRegex);
45
+ // Match regular space followed by particle (not nbsp)
46
+ // Note: \b doesn't work with Cyrillic, so we use (?: |$) for word end
47
+ // Note: \s includes nbsp (U+00A0), so we explicitly use space character
48
+ return new RegExp(
49
+ ` +(${escaped.join('|')})(?:$| )`,
50
+ 'gi'
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Checks if text is already wrapped in JSX expression
56
+ */
57
+ function isInJSXExpression(node) {
58
+ return node.parent && node.parent.type === 'JSXExpressionContainer';
59
+ }
60
+
61
+ /**
62
+ * Replaces regular spaces with non-breaking spaces after prepositions and before particles
63
+ * Returns text with unicode escape sequences (\u00A0) instead of actual nbsp characters
64
+ */
65
+ function fixText(text) {
66
+ const prepositionPattern = createPrepositionAfterPattern();
67
+ const particlePattern = createParticleBeforePattern();
68
+
69
+ let fixed = text;
70
+
71
+ // Replace space after prepositions with nbsp literal
72
+ fixed = fixed.replace(prepositionPattern, (match, preposition) => {
73
+ // match includes leading whitespace, so preserve it
74
+ const leadingSpace = match.startsWith(' ') || match.startsWith('\n') || match.startsWith('\t') ? match[0] : '';
75
+ return leadingSpace + preposition + '\\u00A0';
76
+ });
77
+
78
+ // Replace space before particles with nbsp literal
79
+ fixed = fixed.replace(particlePattern, (match, particle) => {
80
+ // Check if particle is followed by space or end
81
+ const trailingSpace = match.endsWith(' ') || match.endsWith('\n') || match.endsWith('\t') ? match[match.length - 1] : '';
82
+ return '\\u00A0' + particle + trailingSpace;
83
+ });
84
+
85
+ return fixed;
86
+ }
87
+
88
+ /**
89
+ * Checks if text needs fixing
90
+ */
91
+ function needsFix(text) {
92
+ const prepositionPattern = createPrepositionAfterPattern();
93
+ const particlePattern = createParticleBeforePattern();
94
+
95
+ return prepositionPattern.test(text) || particlePattern.test(text);
96
+ }
97
+
98
+ module.exports = {
99
+ meta: {
100
+ type: 'layout',
101
+ docs: {
102
+ description: 'Prevents hanging prepositions in Russian text by enforcing non-breaking spaces',
103
+ category: 'Stylistic Issues',
104
+ recommended: true,
105
+ },
106
+ fixable: 'whitespace',
107
+ schema: [],
108
+ messages: {
109
+ hangingPreposition: 'Preposition "{{word}}" should be followed by a non-breaking space (\\u00A0) to prevent hanging',
110
+ hangingParticle: 'Particle "{{word}}" should be preceded by a non-breaking space (\\u00A0) to prevent hanging',
111
+ },
112
+ },
113
+
114
+ create(context) {
115
+ return {
116
+ JSXText(node) {
117
+ // Skip if already in JSX expression
118
+ if (isInJSXExpression(node)) {
119
+ return;
120
+ }
121
+
122
+ const text = node.value;
123
+
124
+ // Skip empty or whitespace-only text
125
+ if (!text || !text.trim()) {
126
+ return;
127
+ }
128
+
129
+ // Check if text needs fixing
130
+ if (!needsFix(text)) {
131
+ return;
132
+ }
133
+
134
+ const sourceCode = context.getSourceCode();
135
+ const prepositionPattern = createPrepositionAfterPattern();
136
+ const particlePattern = createParticleBeforePattern();
137
+
138
+ // Find all violations for prepositions
139
+ let match;
140
+ while ((match = prepositionPattern.exec(text)) !== null) {
141
+ const preposition = match[1];
142
+
143
+ context.report({
144
+ node,
145
+ messageId: 'hangingPreposition',
146
+ data: {
147
+ word: preposition,
148
+ },
149
+ fix(fixer) {
150
+ const fixedText = fixText(text);
151
+ // Wrap in JSX expression with template literal
152
+ const replacement = `{\`${fixedText}\`}`;
153
+ return fixer.replaceText(node, replacement);
154
+ },
155
+ });
156
+
157
+ // Report only once per node to avoid duplicate fixes
158
+ break;
159
+ }
160
+
161
+ // Reset regex lastIndex
162
+ prepositionPattern.lastIndex = 0;
163
+
164
+ // Find all violations for particles (only if no preposition violations found)
165
+ if (!match) {
166
+ while ((match = particlePattern.exec(text)) !== null) {
167
+ const particle = match[1];
168
+
169
+ context.report({
170
+ node,
171
+ messageId: 'hangingParticle',
172
+ data: {
173
+ word: particle,
174
+ },
175
+ fix(fixer) {
176
+ const fixedText = fixText(text);
177
+ // Wrap in JSX expression with template literal
178
+ const replacement = `{\`${fixedText}\`}`;
179
+ return fixer.replaceText(node, replacement);
180
+ },
181
+ });
182
+
183
+ // Report only once per node
184
+ break;
185
+ }
186
+ }
187
+ },
188
+ };
189
+ },
190
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@m4reve/eslint-plugin-ru-preposition-validator",
3
+ "version": "1.0.0",
4
+ "description": "ESLint plugin for Russian typography rules - prevents hanging prepositions by inserting non-breaking spaces",
5
+ "main": "lib/index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/m4reve/eslint-plugin-ru-preposition-validator.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/m4reve/eslint-plugin-ru-preposition-validator/issues"
12
+ },
13
+ "homepage": "https://github.com/m4reve/eslint-plugin-ru-preposition-validator#readme",
14
+ "scripts": {
15
+ "test": "jest"
16
+ },
17
+ "keywords": [
18
+ "eslint",
19
+ "eslint-plugin",
20
+ "russian",
21
+ "typography",
22
+ "prepositions",
23
+ "nbsp",
24
+ "non-breaking-space"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "eslint": ">=7.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "eslint": "^8.57.0",
33
+ "jest": "^29.7.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=12.0.0"
37
+ }
38
+ }