@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/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;
@@ -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 };
@@ -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)