@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 +26 -0
- package/CONTRIBUTING.md +135 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/examples/after.jsx +44 -0
- package/examples/before.jsx +44 -0
- package/lib/index.js +20 -0
- package/lib/rules/no-hanging-prepositions.js +190 -0
- package/package.json +38 -0
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)
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+
}
|