@consilioweb/spellcheck 0.10.1
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/LICENSE +21 -0
- package/README.md +567 -0
- package/dist/client.cjs +1711 -0
- package/dist/client.d.cts +77 -0
- package/dist/client.d.ts +77 -0
- package/dist/client.js +1702 -0
- package/dist/index.cjs +1691 -0
- package/dist/index.d.cts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +1677 -0
- package/dist/views.cjs +32 -0
- package/dist/views.d.cts +11 -0
- package/dist/views.d.ts +11 -0
- package/dist/views.js +30 -0
- package/package.json +102 -0
- package/scripts/debug-check.mjs +295 -0
- package/scripts/install.mjs +236 -0
- package/scripts/uninstall.mjs +350 -0
package/dist/views.cjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var templates = require('@payloadcms/next/templates');
|
|
4
|
+
var navigation = require('next/navigation');
|
|
5
|
+
var client = require('@consilioweb/spellcheck/client');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/views/SpellCheckView.tsx
|
|
9
|
+
var SpellCheckView = (props) => {
|
|
10
|
+
const { initPageResult } = props;
|
|
11
|
+
if (!initPageResult?.req?.user) {
|
|
12
|
+
navigation.redirect("/admin/login");
|
|
13
|
+
}
|
|
14
|
+
const { req, visibleEntities, permissions, locale } = initPageResult;
|
|
15
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16
|
+
templates.DefaultTemplate,
|
|
17
|
+
{
|
|
18
|
+
i18n: req.i18n,
|
|
19
|
+
locale,
|
|
20
|
+
params: {},
|
|
21
|
+
payload: req.payload,
|
|
22
|
+
permissions,
|
|
23
|
+
req,
|
|
24
|
+
searchParams: {},
|
|
25
|
+
user: req.user,
|
|
26
|
+
visibleEntities,
|
|
27
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(client.SpellCheckDashboard, {})
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
exports.SpellCheckView = SpellCheckView;
|
package/dist/views.d.cts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AdminViewServerProps } from 'payload';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SpellCheckView — Server component wrapper.
|
|
6
|
+
* Wraps the client dashboard in Payload's DefaultTemplate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare const SpellCheckView: React.FC<AdminViewServerProps>;
|
|
10
|
+
|
|
11
|
+
export { SpellCheckView };
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AdminViewServerProps } from 'payload';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SpellCheckView — Server component wrapper.
|
|
6
|
+
* Wraps the client dashboard in Payload's DefaultTemplate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare const SpellCheckView: React.FC<AdminViewServerProps>;
|
|
10
|
+
|
|
11
|
+
export { SpellCheckView };
|
package/dist/views.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { SpellCheckDashboard } from '@consilioweb/spellcheck/client';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/views/SpellCheckView.tsx
|
|
7
|
+
var SpellCheckView = (props) => {
|
|
8
|
+
const { initPageResult } = props;
|
|
9
|
+
if (!initPageResult?.req?.user) {
|
|
10
|
+
redirect("/admin/login");
|
|
11
|
+
}
|
|
12
|
+
const { req, visibleEntities, permissions, locale } = initPageResult;
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
DefaultTemplate,
|
|
15
|
+
{
|
|
16
|
+
i18n: req.i18n,
|
|
17
|
+
locale,
|
|
18
|
+
params: {},
|
|
19
|
+
payload: req.payload,
|
|
20
|
+
permissions,
|
|
21
|
+
req,
|
|
22
|
+
searchParams: {},
|
|
23
|
+
user: req.user,
|
|
24
|
+
visibleEntities,
|
|
25
|
+
children: /* @__PURE__ */ jsx(SpellCheckDashboard, {})
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { SpellCheckView };
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@consilioweb/spellcheck",
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"description": "Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/client.d.ts",
|
|
23
|
+
"default": "./dist/client.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/client.d.cts",
|
|
27
|
+
"default": "./dist/client.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./views": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/views.d.ts",
|
|
33
|
+
"default": "./dist/views.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/views.d.cts",
|
|
37
|
+
"default": "./dist/views.cjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"bin": {
|
|
42
|
+
"spellcheck-install": "./scripts/install.mjs",
|
|
43
|
+
"spellcheck-uninstall": "./scripts/uninstall.mjs"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"scripts",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"keywords": [
|
|
52
|
+
"spellcheck",
|
|
53
|
+
"spelling",
|
|
54
|
+
"grammar",
|
|
55
|
+
"languagetool",
|
|
56
|
+
"payload",
|
|
57
|
+
"payload-cms",
|
|
58
|
+
"payload-plugin",
|
|
59
|
+
"lexical",
|
|
60
|
+
"i18n",
|
|
61
|
+
"french",
|
|
62
|
+
"typescript"
|
|
63
|
+
],
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/pOwn3d/payload-spellcheck.git"
|
|
67
|
+
},
|
|
68
|
+
"author": "ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)",
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@payloadcms/next": "^3.0.0",
|
|
72
|
+
"@payloadcms/ui": "^3.0.0",
|
|
73
|
+
"payload": "^3.0.0",
|
|
74
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@payloadcms/ui": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"@payloadcms/next": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"react": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18"
|
|
89
|
+
},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@types/react": "^19.0.0",
|
|
92
|
+
"tsup": "^8.0.0",
|
|
93
|
+
"typescript": "^5.5.0",
|
|
94
|
+
"vitest": "^2.0.0"
|
|
95
|
+
},
|
|
96
|
+
"scripts": {
|
|
97
|
+
"build": "tsup",
|
|
98
|
+
"test": "vitest run",
|
|
99
|
+
"test:watch": "vitest",
|
|
100
|
+
"typecheck": "tsc --noEmit"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Debug script — test text extraction + LanguageTool on a specific page.
|
|
4
|
+
* Usage: node scripts/debug-check.mjs <url-or-slug> [api-base]
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* node scripts/debug-check.mjs application-mobile/expo-react-native https://consilioweb.fr
|
|
8
|
+
* node scripts/debug-check.mjs agence-web-correze https://consilioweb.fr
|
|
9
|
+
* node scripts/debug-check.mjs agence-web-correze http://localhost:3003
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const slug = process.argv[2]
|
|
13
|
+
const apiBase = process.argv[3] || 'https://consilioweb.fr'
|
|
14
|
+
|
|
15
|
+
if (!slug) {
|
|
16
|
+
console.error('Usage: node scripts/debug-check.mjs <slug> [api-base]')
|
|
17
|
+
console.error(' Ex: node scripts/debug-check.mjs agence-web-correze https://consilioweb.fr')
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LANGUAGETOOL_API = 'https://api.languagetool.org/v2/check'
|
|
22
|
+
|
|
23
|
+
// ---- Text extraction (simplified from lexicalParser.ts) ----
|
|
24
|
+
|
|
25
|
+
function isLexicalJson(value) {
|
|
26
|
+
if (!value || typeof value !== 'object') return false
|
|
27
|
+
return Boolean(
|
|
28
|
+
(value.root && typeof value.root === 'object') ||
|
|
29
|
+
(Array.isArray(value.children) && value.type !== undefined),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractTextFromLexical(node, maxDepth = 50) {
|
|
34
|
+
return extractRecursive(node, 0, maxDepth).trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractRecursive(node, depth, maxDepth) {
|
|
38
|
+
if (!node || depth > maxDepth) return ''
|
|
39
|
+
if (Array.isArray(node)) return node.map(n => extractRecursive(n, depth + 1, maxDepth)).join('')
|
|
40
|
+
if (typeof node !== 'object') return ''
|
|
41
|
+
|
|
42
|
+
const SKIP = new Set(['code', 'code-block', 'codeBlock'])
|
|
43
|
+
if (node.type && SKIP.has(node.type)) return ''
|
|
44
|
+
|
|
45
|
+
let text = ''
|
|
46
|
+
|
|
47
|
+
if (node.type === 'text' && typeof node.text === 'string') {
|
|
48
|
+
text += node.text // NO extra space — this is the v0.6.0 fix
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (node.type === 'paragraph' || node.type === 'heading' || node.type === 'listitem') {
|
|
52
|
+
for (const child of node.children || []) {
|
|
53
|
+
text += extractRecursive(child, depth + 1, maxDepth)
|
|
54
|
+
}
|
|
55
|
+
text += '\n'
|
|
56
|
+
return text
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(node.children)) {
|
|
60
|
+
for (const child of node.children) {
|
|
61
|
+
text += extractRecursive(child, depth + 1, maxDepth)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (node.root) {
|
|
66
|
+
text += extractRecursive(node.root, depth + 1, maxDepth)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return text
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// stripHtml removed in v0.9.5 — Lexical stores plain text, not HTML.
|
|
73
|
+
|
|
74
|
+
function extractAllTextFromDoc(doc) {
|
|
75
|
+
const texts = []
|
|
76
|
+
if (doc.title && typeof doc.title === 'string') texts.push(doc.title)
|
|
77
|
+
if (doc.hero?.richText) texts.push(extractTextFromLexical(doc.hero.richText))
|
|
78
|
+
if (doc.content && isLexicalJson(doc.content)) texts.push(extractTextFromLexical(doc.content))
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(doc.layout)) {
|
|
81
|
+
for (const block of doc.layout) {
|
|
82
|
+
extractTextFromBlock(block, texts, new WeakSet())
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return texts.filter(Boolean).join('\n').trim()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SKIP_KEYS = new Set([
|
|
89
|
+
'id', '_order', '_parent_id', '_path', '_locale', '_uuid',
|
|
90
|
+
'blockType', 'blockName', 'icon', 'color', 'link', 'link_url',
|
|
91
|
+
'enable_link', 'image', 'media', 'form', 'form_id', 'rating',
|
|
92
|
+
'size', 'position', 'relationTo', 'value', 'updatedAt', 'createdAt',
|
|
93
|
+
'_status', 'slug', 'meta', 'publishedAt', 'populatedAuthors',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
const TEXT_FIELDS = ['title', 'description', 'heading', 'subheading', 'subtitle',
|
|
97
|
+
'quote', 'author', 'role', 'label', 'link_label', 'block_name',
|
|
98
|
+
'caption', 'alt', 'text', 'summary', 'excerpt']
|
|
99
|
+
|
|
100
|
+
function extractTextFromBlock(obj, texts, visited, depth = 0) {
|
|
101
|
+
if (!obj || typeof obj !== 'object' || depth > 10) return
|
|
102
|
+
if (visited.has(obj)) return
|
|
103
|
+
visited.add(obj)
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(obj)) {
|
|
106
|
+
for (const item of obj) extractTextFromBlock(item, texts, visited, depth + 1)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
111
|
+
if (SKIP_KEYS.has(key)) continue
|
|
112
|
+
if (isLexicalJson(value)) { texts.push(extractTextFromLexical(value)); continue }
|
|
113
|
+
if (typeof value === 'string' && value.length > 2 && value.length < 5000) {
|
|
114
|
+
if (/^(https?:|\/|#|\d{4}-\d{2}|[0-9a-f-]{36}|data:|mailto:)/i.test(value)) continue
|
|
115
|
+
if (/^\{.*\}$/.test(value) || /^\[.*\]$/.test(value)) continue
|
|
116
|
+
if (TEXT_FIELDS.includes(key)) texts.push(value)
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === 'object' && value !== null) {
|
|
119
|
+
extractTextFromBlock(value, texts, visited, depth + 1)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- LanguageTool check ----
|
|
125
|
+
|
|
126
|
+
async function checkWithLanguageTool(text, language = 'fr') {
|
|
127
|
+
const disabledRules = [
|
|
128
|
+
'WHITESPACE_RULE', 'COMMA_PARENTHESIS_WHITESPACE', 'UNPAIRED_BRACKETS',
|
|
129
|
+
'FR_SPELLING_RULE',
|
|
130
|
+
].join(',')
|
|
131
|
+
|
|
132
|
+
const params = new URLSearchParams({ text: text.slice(0, 18000), language, disabledRules })
|
|
133
|
+
|
|
134
|
+
const res = await fetch(LANGUAGETOOL_API, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
137
|
+
body: params.toString(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (!res.ok) throw new Error(`LanguageTool ${res.status}`)
|
|
141
|
+
const data = await res.json()
|
|
142
|
+
return data.matches || []
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- Filters (from filters.ts) ----
|
|
146
|
+
|
|
147
|
+
const DEFAULT_SKIP_RULES = new Set([
|
|
148
|
+
'WHITESPACE_RULE', 'COMMA_PARENTHESIS_WHITESPACE', 'UNPAIRED_BRACKETS',
|
|
149
|
+
'UPPERCASE_SENTENCE_START', 'FRENCH_WHITESPACE', 'MORFOLOGIK_RULE_FR_FR',
|
|
150
|
+
'APOS_TYP', 'APOS_INCORRECT', 'POINT_VIRGULE', 'DASH_RULE', 'FR_SPELLING_RULE',
|
|
151
|
+
'FRENCH_WORD_REPEAT_RULE', 'MOT_TRAIT_MOT', 'PAS_DE_TRAIT_UNION',
|
|
152
|
+
'D_N', 'DOUBLES_ESPACES', 'ESPACE_ENTRE_VIRGULE_ET_MOT', 'ESPACE_ENTRE_POINT_ET_MOT',
|
|
153
|
+
'PRONOMS_PERSONNELS_MINUSCULE', 'DET_MAJ_SENT_START', 'FR_SPLIT_WORDS_HYPHEN',
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
const DEFAULT_SKIP_CATEGORIES = new Set([
|
|
157
|
+
'TYPOGRAPHY', 'TYPOS', 'STYLE',
|
|
158
|
+
'CAT_TYPOGRAPHIE', 'REPETITIONS_STYLE', 'CAT_REGLES_DE_BASEE',
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
const CUSTOM_DICTIONARY = [
|
|
162
|
+
'ConsilioWEB', 'Next.js', 'Payload', 'TypeScript', 'JavaScript',
|
|
163
|
+
'React', 'React Native', 'Expo', 'Flutter', 'Node.js', 'Tailwind', 'cross-platform',
|
|
164
|
+
'mobile-first', 'utility-first', 'multi-appareils',
|
|
165
|
+
'useMemo', 'useCallback', 'useEffect', 'useState', 'useRef',
|
|
166
|
+
'SEO', 'RGPD', 'RGESN', 'CMS', 'API', 'CSS', 'HTML', 'PHP',
|
|
167
|
+
'WordPress', 'PrestaShop', 'Symfony', 'WooCommerce', 'Shopify',
|
|
168
|
+
'Matomo', 'n8n', 'Figma', 'Vercel', 'Infomaniak',
|
|
169
|
+
'Corrèze', 'Limousin', 'Ussel', 'Tulle', 'Brive', 'Limoges',
|
|
170
|
+
'Aurillac', 'Clermont-Ferrand', 'Nouvelle-Aquitaine',
|
|
171
|
+
// Hosting & brands
|
|
172
|
+
'o2switch', 'PlanetHoster', 'OVH', 'Brevo', 'Whitespark',
|
|
173
|
+
// English tech terms
|
|
174
|
+
'pull request', 'pull requests', 'brute force', 'rich snippets',
|
|
175
|
+
'lazy loading', 'code splitting', 'tree shaking', 'hot reload',
|
|
176
|
+
'Content-Security-Policy', 'X-Frame-Options', 'Strict-Transport-Security',
|
|
177
|
+
// Multi-word tech terms (context-aware matching)
|
|
178
|
+
'variable fonts', 'container queries', 'media query', 'media queries',
|
|
179
|
+
'server components', 'server actions', 'App Router', 'use cache',
|
|
180
|
+
'pré-rendre', 'pré-rendu', 'pré-rendue', 'pré-rendues',
|
|
181
|
+
].map(w => w.toLowerCase())
|
|
182
|
+
|
|
183
|
+
function filterMatch(m) {
|
|
184
|
+
const ruleId = m.rule.id
|
|
185
|
+
const category = m.rule.category.id
|
|
186
|
+
const original = m.context.text.slice(m.context.offset, m.context.offset + m.context.length)
|
|
187
|
+
const isPremium = m.rule.isPremium
|
|
188
|
+
|
|
189
|
+
if (isPremium) return { skip: true, reason: 'premium rule' }
|
|
190
|
+
if (DEFAULT_SKIP_RULES.has(ruleId)) return { skip: true, reason: `skip rule: ${ruleId}` }
|
|
191
|
+
if (DEFAULT_SKIP_CATEGORIES.has(category)) return { skip: true, reason: `skip category: ${category}` }
|
|
192
|
+
|
|
193
|
+
if (original) {
|
|
194
|
+
const lower = original.toLowerCase()
|
|
195
|
+
for (const word of CUSTOM_DICTIONARY) {
|
|
196
|
+
if (lower.includes(word) || word.includes(lower)) {
|
|
197
|
+
return { skip: true, reason: `dictionary: "${word}"` }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Context-aware multi-word dictionary check
|
|
203
|
+
if (m.context && m.context.text) {
|
|
204
|
+
const ctxLower = m.context.text.toLowerCase()
|
|
205
|
+
for (const word of CUSTOM_DICTIONARY) {
|
|
206
|
+
if (word.includes(' ') && ctxLower.includes(word)) {
|
|
207
|
+
return { skip: true, reason: `context dict: "${word}"` }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (original && original.length <= 1 && category !== 'GRAMMAR') {
|
|
213
|
+
return { skip: true, reason: 'single char, not grammar' }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (ruleId.includes('REPET') || category === 'CAT_REGLES_DE_BASE') {
|
|
217
|
+
if (original) {
|
|
218
|
+
const lower = original.toLowerCase()
|
|
219
|
+
for (const word of CUSTOM_DICTIONARY) {
|
|
220
|
+
if (lower.includes(word) || word.includes(lower)) {
|
|
221
|
+
return { skip: true, reason: `repetition of dict word: "${word}"` }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { skip: false, reason: null }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- Main ----
|
|
231
|
+
|
|
232
|
+
async function main() {
|
|
233
|
+
console.log(`\n🔍 Debug spellcheck: slug="${slug}" api="${apiBase}"\n`)
|
|
234
|
+
|
|
235
|
+
// 1. Fetch page
|
|
236
|
+
console.log('📄 Fetching page...')
|
|
237
|
+
const pageRes = await fetch(`${apiBase}/api/pages?where[slug][equals]=${slug}&depth=1&limit=1`)
|
|
238
|
+
let data = await pageRes.json()
|
|
239
|
+
let doc = data.docs?.[0]
|
|
240
|
+
|
|
241
|
+
if (!doc) {
|
|
242
|
+
// Try posts
|
|
243
|
+
const postRes = await fetch(`${apiBase}/api/posts?where[slug][equals]=${slug}&depth=1&limit=1`)
|
|
244
|
+
data = await postRes.json()
|
|
245
|
+
doc = data.docs?.[0]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!doc) {
|
|
249
|
+
console.error(`❌ Document "${slug}" not found`)
|
|
250
|
+
process.exit(1)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(` Title: ${doc.title}`)
|
|
254
|
+
console.log(` ID: ${doc.id}`)
|
|
255
|
+
|
|
256
|
+
// 2. Extract text
|
|
257
|
+
console.log('\n📝 Extracting text...')
|
|
258
|
+
const text = extractAllTextFromDoc(doc)
|
|
259
|
+
console.log(` Words: ${text.split(/\s+/).filter(w => w.length > 0).length}`)
|
|
260
|
+
console.log(` Length: ${text.length} chars`)
|
|
261
|
+
console.log('\n--- EXTRACTED TEXT (first 2000 chars) ---')
|
|
262
|
+
console.log(text.slice(0, 2000))
|
|
263
|
+
console.log('--- END ---\n')
|
|
264
|
+
|
|
265
|
+
// 3. Check with LanguageTool
|
|
266
|
+
console.log('🔎 Checking with LanguageTool...')
|
|
267
|
+
const matches = await checkWithLanguageTool(text)
|
|
268
|
+
console.log(` Raw matches: ${matches.length}\n`)
|
|
269
|
+
|
|
270
|
+
// 4. Show all matches with filter decisions
|
|
271
|
+
let kept = 0
|
|
272
|
+
let filtered = 0
|
|
273
|
+
|
|
274
|
+
for (const m of matches) {
|
|
275
|
+
const original = m.context.text.slice(m.context.offset, m.context.offset + m.context.length)
|
|
276
|
+
const replacement = m.replacements?.[0]?.value || '(none)'
|
|
277
|
+
const { skip, reason } = filterMatch(m)
|
|
278
|
+
|
|
279
|
+
if (skip) {
|
|
280
|
+
filtered++
|
|
281
|
+
console.log(` ⏭️ FILTERED: "${original}" → "${replacement}"`)
|
|
282
|
+
console.log(` Rule: ${m.rule.id} | Cat: ${m.rule.category.id} | Reason: ${reason}`)
|
|
283
|
+
} else {
|
|
284
|
+
kept++
|
|
285
|
+
console.log(` ⚠️ KEPT: "${original}" → "${replacement}"`)
|
|
286
|
+
console.log(` Rule: ${m.rule.id} | Cat: ${m.rule.category.id} | ${m.message}`)
|
|
287
|
+
console.log(` Context: ...${m.context.text}...`)
|
|
288
|
+
}
|
|
289
|
+
console.log()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`\n📊 Summary: ${matches.length} raw → ${kept} kept, ${filtered} filtered out\n`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
main().catch(console.error)
|