reedach-server 0.2.0 → 0.3.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/README.md +5 -0
- package/package.json +3 -3
- package/src/check.mjs +2 -2
- package/src/morph.mjs +29 -0
package/README.md
CHANGED
|
@@ -91,6 +91,9 @@ docker run -d -p 3000:3000 -e REEDACH_KEYS="длинный-ключ-сайта=h
|
|
|
91
91
|
У остальных ошибок `suggestions: null`: варианты можно запросить отдельно, когда автор
|
|
92
92
|
нажмёт на слово.
|
|
93
93
|
|
|
94
|
+
У подсказок по стилю может быть поле `replace` — варианты замены, уже поставленные в форму
|
|
95
|
+
найденного слова: «релевантных» → `["подходящих", "нужных", "уместных"]`.
|
|
96
|
+
|
|
94
97
|
**`POST /v1/suggest`** — `{"words": ["превет"]}` → `{"suggestions": {"превет": ["привет", …]}}`,
|
|
95
98
|
до 20 слов за раз.
|
|
96
99
|
|
|
@@ -146,4 +149,6 @@ docker run -d -p 3000:3000 -e REEDACH_KEYS="длинный-ключ-сайта=h
|
|
|
146
149
|
- зависший процесс убивается и запускается заново, а после 2000 слов процесс
|
|
147
150
|
перезапускается в любом случае.
|
|
148
151
|
|
|
152
|
+
История версий всех пакетов — в [CHANGELOG.md](https://github.com/dimafaller-en/reedach/blob/main/CHANGELOG.md).
|
|
153
|
+
|
|
149
154
|
MIT, © 2026 Дима Фаллер. Сторонние словари и программы — в NOTICE.md.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reedach-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Сервер Reedach: орфография (hunspell), морфология (Az.js + OpenCorpora) и подсказки по стилю для русского текста — библиотекой или по HTTP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dima Faller <dima@dimafaller.ru>",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"az": "0.2.3",
|
|
42
42
|
"dictionary-en": "4.0.0",
|
|
43
43
|
"dictionary-ru": "3.0.0",
|
|
44
|
-
"reedach": "0.
|
|
44
|
+
"reedach": "0.3.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"reedach-widget": "^0.
|
|
47
|
+
"reedach-widget": "^0.3.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"reedach-widget": {
|
package/src/check.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {checkStyle,sourceBlocks,textBlocks,mergeIssues,RULES,BUILTIN,CATEGORIES} from 'reedach';
|
|
2
|
-
import {morphCheck,MORPH_RULES} from './morph.mjs';
|
|
2
|
+
import {morphCheck,MORPH_RULES,inflectReplacements} from './morph.mjs';
|
|
3
3
|
import {words,known,misspelled,Suggester} from './spell.mjs';
|
|
4
4
|
// One check = spelling (hunspell) + morphology (Az.js) + dictionary style hints (reedach core), merged into one list.
|
|
5
5
|
export const CHECKS=['spelling','morph','style'];
|
|
@@ -27,7 +27,7 @@ export async function check(input,{suggester:sg=suggester(),misspelled:ms=misspe
|
|
|
27
27
|
checks.has('spelling')?findSpelling(blocks,ignore,sg,lim,ms):[],
|
|
28
28
|
checks.has('morph')?morphCheck(blocks).catch(()=>[]):[],
|
|
29
29
|
]);
|
|
30
|
-
const local=checks.has('style')?checkStyle(blocks):[];
|
|
30
|
+
const local=checks.has('style')?await inflectReplacements(checkStyle(blocks),blocks).catch(()=>checkStyle(blocks)):[];
|
|
31
31
|
const issues=mergeIssues(blocks,{spelling,style:morph,local}).filter(it=>!disable.has(it.id)&&!disable.has(it.category));
|
|
32
32
|
return {issues:blocksInput?issues.sort((a,b)=>a.block-b.block||a.start-b.start):issues.map(({block,...it})=>it)};
|
|
33
33
|
}
|
package/src/morph.mjs
CHANGED
|
@@ -59,3 +59,32 @@ export async function morphCheck(blocks){
|
|
|
59
59
|
return issues;
|
|
60
60
|
}
|
|
61
61
|
export const MORPH_RULES=RULES;
|
|
62
|
+
|
|
63
|
+
// A click puts the replacement in place of the found word, so it takes that word's form: «релевантные» → «подходящие».
|
|
64
|
+
// The parse of the found word is the one with the replacement's part of speech («фичи» is a noun here, not a verb);
|
|
65
|
+
// a noun keeps its own gender and, when its base form is plural («возможности»), its number.
|
|
66
|
+
const FAMILY={INFN:'verb',VERB:'verb',ADJF:'adj',PRTF:'adj',ADJS:'adjs',PRTS:'adjs',NOUN:'noun',ADVB:'advb'};
|
|
67
|
+
const KEEP={verb:['NMbr','TEns','PErs','GNdr','MOod','INvl'],adj:['NMbr','CAse','GNdr','ANim'],adjs:['NMbr','GNdr'],noun:['NMbr','CAse'],advb:[]};
|
|
68
|
+
function inflect(found,word){
|
|
69
|
+
const lower=found.toLowerCase();if(lower.length>MAX_WORD||word.length>MAX_WORD)return word;
|
|
70
|
+
let parses,targets;try{parses=Az.Morph(lower);targets=Az.Morph(word);}catch{return word;}
|
|
71
|
+
for(const f of parses){
|
|
72
|
+
const fam=FAMILY[f.tag.POS],t=fam&&targets.find(x=>FAMILY[x.tag.POS]===fam);if(!t)continue;
|
|
73
|
+
if(fam==='advb')return word;
|
|
74
|
+
const keep=KEEP[fam].filter(k=>!(fam==='noun'&&k==='NMbr'&&t.tag.NMbr==='plur'));
|
|
75
|
+
const want=keep.map(k=>f.tag[k]).filter(Boolean);if(fam==='noun'&&t.tag.NMbr==='plur')want.push('plur');
|
|
76
|
+
if(fam==='verb'&&f.tag.POS==='VERB'&&t.tag.POS==='INFN')want.push('VERB');
|
|
77
|
+
if(f.tag.POS==='INFN')return t.normalize()?.word||word;
|
|
78
|
+
try{const r=t.inflect(want);if(r?.word)return r.word;}catch{}
|
|
79
|
+
return word;
|
|
80
|
+
}
|
|
81
|
+
return word;
|
|
82
|
+
}
|
|
83
|
+
// issues from checkStyle on the same blocks: variants (dictionary forms) become replacements in the found word's form.
|
|
84
|
+
export async function inflectReplacements(issues,blocks){
|
|
85
|
+
const todo=issues.filter(it=>it.variants);if(!todo.length)return issues;
|
|
86
|
+
await init();
|
|
87
|
+
// In a phrase («личного бренда», «веб-сайта») the form comes from its last word.
|
|
88
|
+
for(const it of todo){const found=(blocks[it.block]?.text.slice(it.start,it.end)||'').split(/[\s-]+/).pop();if(/^[А-Яа-яЁё]+$/.test(found))it.replace=it.variants.map(w=>inflect(found,w));}
|
|
89
|
+
return issues;
|
|
90
|
+
}
|