design-learn-server 0.1.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.
Files changed (37) hide show
  1. package/README.md +123 -0
  2. package/package.json +29 -0
  3. package/src/cli.js +152 -0
  4. package/src/mcp/index.js +556 -0
  5. package/src/pipeline/index.js +335 -0
  6. package/src/playwrightSupport.js +65 -0
  7. package/src/preview/index.js +204 -0
  8. package/src/server.js +1385 -0
  9. package/src/stdio.js +464 -0
  10. package/src/storage/fileStore.js +45 -0
  11. package/src/storage/index.js +983 -0
  12. package/src/storage/paths.js +113 -0
  13. package/src/storage/sqliteStore.js +114 -0
  14. package/src/uipro/bm25.js +121 -0
  15. package/src/uipro/config.js +264 -0
  16. package/src/uipro/csv.js +90 -0
  17. package/src/uipro/data/charts.csv +26 -0
  18. package/src/uipro/data/colors.csv +97 -0
  19. package/src/uipro/data/icons.csv +101 -0
  20. package/src/uipro/data/landing.csv +31 -0
  21. package/src/uipro/data/products.csv +97 -0
  22. package/src/uipro/data/prompts.csv +24 -0
  23. package/src/uipro/data/stacks/flutter.csv +53 -0
  24. package/src/uipro/data/stacks/html-tailwind.csv +56 -0
  25. package/src/uipro/data/stacks/nextjs.csv +53 -0
  26. package/src/uipro/data/stacks/nuxt-ui.csv +51 -0
  27. package/src/uipro/data/stacks/nuxtjs.csv +59 -0
  28. package/src/uipro/data/stacks/react-native.csv +52 -0
  29. package/src/uipro/data/stacks/react.csv +54 -0
  30. package/src/uipro/data/stacks/shadcn.csv +61 -0
  31. package/src/uipro/data/stacks/svelte.csv +54 -0
  32. package/src/uipro/data/stacks/swiftui.csv +51 -0
  33. package/src/uipro/data/stacks/vue.csv +50 -0
  34. package/src/uipro/data/styles.csv +59 -0
  35. package/src/uipro/data/typography.csv +58 -0
  36. package/src/uipro/data/ux-guidelines.csv +100 -0
  37. package/src/uipro/index.js +581 -0
@@ -0,0 +1,113 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+
4
+ function expandHome(inputPath) {
5
+ if (!inputPath || typeof inputPath !== 'string') {
6
+ return inputPath;
7
+ }
8
+
9
+ if (inputPath === '~') {
10
+ return os.homedir();
11
+ }
12
+
13
+ if (inputPath.startsWith('~/')) {
14
+ return path.join(os.homedir(), inputPath.slice(2));
15
+ }
16
+
17
+ return inputPath;
18
+ }
19
+
20
+ function resolveDataDir(override) {
21
+ const candidate =
22
+ override || process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR;
23
+ if (candidate) {
24
+ return expandHome(candidate);
25
+ }
26
+
27
+ return path.join(os.homedir(), '.design-learn', 'data');
28
+ }
29
+
30
+ function getDesignDir(dataDir, designId) {
31
+ return path.join(dataDir, 'designs', designId);
32
+ }
33
+
34
+ function getDesignMetaPath(dataDir, designId) {
35
+ return path.join(getDesignDir(dataDir, designId), 'design.json');
36
+ }
37
+
38
+ function getDesignIndexPath(dataDir) {
39
+ return path.join(dataDir, 'designs', '_index.json');
40
+ }
41
+
42
+ function getVersionDir(dataDir, designId, versionNumber) {
43
+ return path.join(getDesignDir(dataDir, designId), `v${versionNumber}`);
44
+ }
45
+
46
+ function getStyleguidePath(dataDir, designId, versionNumber) {
47
+ return path.join(getVersionDir(dataDir, designId, versionNumber), 'styleguide.md');
48
+ }
49
+
50
+ function getRulesPath(dataDir, designId, versionNumber) {
51
+ return path.join(getVersionDir(dataDir, designId, versionNumber), 'rules.json');
52
+ }
53
+
54
+ function getSnapshotsPath(dataDir, designId, versionNumber) {
55
+ return path.join(getVersionDir(dataDir, designId, versionNumber), 'snapshots.json');
56
+ }
57
+
58
+ function getComponentsDir(dataDir, designId, versionNumber) {
59
+ return path.join(getVersionDir(dataDir, designId, versionNumber), 'components');
60
+ }
61
+
62
+ function getComponentDir(dataDir, designId, versionNumber, componentId) {
63
+ return path.join(getComponentsDir(dataDir, designId, versionNumber), componentId);
64
+ }
65
+
66
+ function getComponentCodePath(dataDir, designId, versionNumber, componentId) {
67
+ return path.join(getComponentDir(dataDir, designId, versionNumber, componentId), 'code.json');
68
+ }
69
+
70
+ function getComponentsIndexPath(dataDir) {
71
+ return path.join(dataDir, 'components', '_index.json');
72
+ }
73
+
74
+ function getRulesDir(dataDir, designId, versionNumber) {
75
+ return path.join(getVersionDir(dataDir, designId, versionNumber), 'rules');
76
+ }
77
+
78
+ function getRulePath(dataDir, designId, versionNumber, ruleId) {
79
+ return path.join(getRulesDir(dataDir, designId, versionNumber), `${ruleId}.json`);
80
+ }
81
+
82
+ function getRulesIndexPath(dataDir) {
83
+ return path.join(dataDir, 'rules', '_index.json');
84
+ }
85
+
86
+ function getDatabasePath(dataDir) {
87
+ return path.join(dataDir, 'database.sqlite');
88
+ }
89
+
90
+ function getConfigPath(dataDir) {
91
+ return path.join(dataDir, 'config.json');
92
+ }
93
+
94
+ module.exports = {
95
+ expandHome,
96
+ resolveDataDir,
97
+ getDesignDir,
98
+ getDesignMetaPath,
99
+ getDesignIndexPath,
100
+ getVersionDir,
101
+ getStyleguidePath,
102
+ getRulesPath,
103
+ getSnapshotsPath,
104
+ getComponentsDir,
105
+ getComponentDir,
106
+ getComponentCodePath,
107
+ getComponentsIndexPath,
108
+ getRulesDir,
109
+ getRulePath,
110
+ getRulesIndexPath,
111
+ getDatabasePath,
112
+ getConfigPath,
113
+ };
@@ -0,0 +1,114 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Database = require('better-sqlite3');
4
+
5
+ const SCHEMA_VERSION = 2;
6
+
7
+ function ensureDir(dirPath) {
8
+ fs.mkdirSync(dirPath, { recursive: true });
9
+ }
10
+
11
+ function openDatabase(dbPath) {
12
+ ensureDir(path.dirname(dbPath));
13
+ const db = new Database(dbPath);
14
+ db.pragma('journal_mode = WAL');
15
+ migrate(db);
16
+ return db;
17
+ }
18
+
19
+ function migrate(db) {
20
+ const version = db.pragma('user_version', { simple: true });
21
+ if (version === SCHEMA_VERSION) {
22
+ return;
23
+ }
24
+
25
+ if (version !== 0 && version !== 1) {
26
+ throw new Error(`Unsupported schema version ${version}`);
27
+ }
28
+
29
+ db.exec(`
30
+ CREATE TABLE IF NOT EXISTS designs (
31
+ id TEXT PRIMARY KEY,
32
+ name TEXT,
33
+ url TEXT,
34
+ source TEXT,
35
+ category TEXT,
36
+ description TEXT,
37
+ thumbnail TEXT,
38
+ stats_json TEXT,
39
+ metadata_json TEXT,
40
+ design_path TEXT,
41
+ created_at TEXT,
42
+ updated_at TEXT
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS versions (
46
+ id TEXT PRIMARY KEY,
47
+ design_id TEXT NOT NULL,
48
+ version_number INTEGER NOT NULL,
49
+ styleguide_path TEXT,
50
+ rules_path TEXT,
51
+ snapshots_path TEXT,
52
+ created_at TEXT,
53
+ created_by TEXT,
54
+ FOREIGN KEY (design_id) REFERENCES designs(id) ON DELETE CASCADE
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS components (
58
+ id TEXT PRIMARY KEY,
59
+ design_id TEXT NOT NULL,
60
+ version_id TEXT NOT NULL,
61
+ name TEXT,
62
+ type TEXT,
63
+ structure_json TEXT,
64
+ code_path TEXT,
65
+ preview_path TEXT,
66
+ created_at TEXT,
67
+ FOREIGN KEY (design_id) REFERENCES designs(id) ON DELETE CASCADE,
68
+ FOREIGN KEY (version_id) REFERENCES versions(id) ON DELETE CASCADE
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS rules (
72
+ id TEXT PRIMARY KEY,
73
+ version_id TEXT NOT NULL,
74
+ type TEXT,
75
+ name TEXT,
76
+ value TEXT,
77
+ raw_path TEXT,
78
+ created_at TEXT,
79
+ FOREIGN KEY (version_id) REFERENCES versions(id) ON DELETE CASCADE
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_versions_design_id ON versions(design_id);
83
+ CREATE INDEX IF NOT EXISTS idx_components_design_id ON components(design_id);
84
+ CREATE INDEX IF NOT EXISTS idx_components_version_id ON components(version_id);
85
+ CREATE INDEX IF NOT EXISTS idx_rules_version_id ON rules(version_id);
86
+ CREATE INDEX IF NOT EXISTS idx_designs_updated_at ON designs(updated_at);
87
+
88
+ -- 任务队列表
89
+ CREATE TABLE IF NOT EXISTS tasks (
90
+ id TEXT PRIMARY KEY,
91
+ url TEXT NOT NULL,
92
+ domain TEXT,
93
+ status TEXT DEFAULT 'pending',
94
+ progress INTEGER DEFAULT 0,
95
+ stage TEXT,
96
+ error TEXT,
97
+ options_json TEXT,
98
+ created_at TEXT,
99
+ updated_at TEXT,
100
+ completed_at TEXT
101
+ );
102
+
103
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
104
+ CREATE INDEX IF NOT EXISTS idx_tasks_domain ON tasks(domain);
105
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at);
106
+ `);
107
+
108
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
109
+ }
110
+
111
+ module.exports = {
112
+ openDatabase,
113
+ SCHEMA_VERSION,
114
+ };
@@ -0,0 +1,121 @@
1
+ const DEFAULT_K1 = 1.5;
2
+ const DEFAULT_B = 0.75;
3
+
4
+ class BM25 {
5
+ constructor(options = {}) {
6
+ this.k1 = Number.isFinite(options.k1) ? options.k1 : DEFAULT_K1;
7
+ this.b = Number.isFinite(options.b) ? options.b : DEFAULT_B;
8
+ this.corpus = [];
9
+ this.termFrequencies = [];
10
+ this.docLengths = [];
11
+ this.avgdl = 0;
12
+ this.idf = new Map();
13
+ this.N = 0;
14
+ }
15
+
16
+ tokenize(text) {
17
+ const normalized = String(text ?? '')
18
+ .toLowerCase()
19
+ // 保留 Unicode 字母/数字 + 空白,避免中文被直接清空
20
+ .replace(/[^\p{L}\p{N}\s]+/gu, ' ')
21
+ .trim();
22
+
23
+ if (!normalized) return [];
24
+
25
+ const tokens = [];
26
+ for (const raw of normalized.split(/\s+/)) {
27
+ if (!raw) continue;
28
+
29
+ // CJK 词:拆成 2-gram,提升“按钮/表单”等短词的可用性
30
+ if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(raw)) {
31
+ if (raw.length === 1) {
32
+ tokens.push(raw);
33
+ continue;
34
+ }
35
+ for (let i = 0; i < raw.length - 1; i += 1) {
36
+ tokens.push(raw.slice(i, i + 2));
37
+ }
38
+ continue;
39
+ }
40
+
41
+ // 英文/数字:允许 2 字符(如 ui/ux/cta)
42
+ if (raw.length >= 2) tokens.push(raw);
43
+ }
44
+
45
+ return tokens;
46
+ }
47
+
48
+ fit(documents) {
49
+ if (!Array.isArray(documents) || documents.length === 0) {
50
+ this.N = 0;
51
+ return;
52
+ }
53
+
54
+ this.corpus = documents.map((doc) => this.tokenize(doc));
55
+ this.N = this.corpus.length;
56
+ this.docLengths = this.corpus.map((tokens) => tokens.length);
57
+ this.avgdl = this.docLengths.reduce((sum, len) => sum + len, 0) / this.N;
58
+
59
+ const docFreqs = new Map();
60
+ this.termFrequencies = this.corpus.map((tokens) => {
61
+ const frequencies = new Map();
62
+ const seen = new Set();
63
+ for (const token of tokens) {
64
+ frequencies.set(token, (frequencies.get(token) || 0) + 1);
65
+ if (!seen.has(token)) {
66
+ docFreqs.set(token, (docFreqs.get(token) || 0) + 1);
67
+ seen.add(token);
68
+ }
69
+ }
70
+ return frequencies;
71
+ });
72
+
73
+ this.idf = new Map();
74
+ for (const [token, freq] of docFreqs.entries()) {
75
+ const idf = Math.log((this.N - freq + 0.5) / (freq + 0.5) + 1);
76
+ this.idf.set(token, idf);
77
+ }
78
+ }
79
+
80
+ score(query) {
81
+ if (this.N === 0) {
82
+ return [];
83
+ }
84
+
85
+ const queryTokens = this.tokenize(query);
86
+ if (queryTokens.length === 0) {
87
+ return [];
88
+ }
89
+
90
+ const scored = [];
91
+ const avgdl = this.avgdl || 1;
92
+ for (let index = 0; index < this.corpus.length; index += 1) {
93
+ const docLen = this.docLengths[index] || 0;
94
+ const frequencies = this.termFrequencies[index];
95
+ let score = 0;
96
+
97
+ for (const token of queryTokens) {
98
+ const idf = this.idf.get(token);
99
+ if (idf === undefined) {
100
+ continue;
101
+ }
102
+ const tf = frequencies.get(token) || 0;
103
+ if (tf === 0) {
104
+ continue;
105
+ }
106
+ const numerator = tf * (this.k1 + 1);
107
+ const denominator = tf + this.k1 * (1 - this.b + (this.b * docLen) / avgdl);
108
+ score += idf * (numerator / denominator);
109
+ }
110
+
111
+ scored.push([index, score]);
112
+ }
113
+
114
+ scored.sort((a, b) => b[1] - a[1]);
115
+ return scored;
116
+ }
117
+ }
118
+
119
+ module.exports = {
120
+ BM25,
121
+ };
@@ -0,0 +1,264 @@
1
+ const DOMAIN_CONFIG = {
2
+ style: {
3
+ file: 'styles.csv',
4
+ searchColumns: ['Style Category', 'Keywords', 'Best For', 'Type'],
5
+ outputColumns: [
6
+ 'Style Category',
7
+ 'Type',
8
+ 'Keywords',
9
+ 'Primary Colors',
10
+ 'Effects & Animation',
11
+ 'Best For',
12
+ 'Performance',
13
+ 'Accessibility',
14
+ 'Framework Compatibility',
15
+ 'Complexity',
16
+ ],
17
+ },
18
+ prompt: {
19
+ file: 'prompts.csv',
20
+ searchColumns: ['Style Category', 'AI Prompt Keywords (Copy-Paste Ready)', 'CSS/Technical Keywords'],
21
+ outputColumns: [
22
+ 'Style Category',
23
+ 'AI Prompt Keywords (Copy-Paste Ready)',
24
+ 'CSS/Technical Keywords',
25
+ 'Implementation Checklist',
26
+ ],
27
+ },
28
+ color: {
29
+ file: 'colors.csv',
30
+ searchColumns: ['Product Type', 'Keywords', 'Notes'],
31
+ outputColumns: [
32
+ 'Product Type',
33
+ 'Keywords',
34
+ 'Primary (Hex)',
35
+ 'Secondary (Hex)',
36
+ 'CTA (Hex)',
37
+ 'Background (Hex)',
38
+ 'Text (Hex)',
39
+ 'Border (Hex)',
40
+ 'Notes',
41
+ ],
42
+ },
43
+ chart: {
44
+ file: 'charts.csv',
45
+ searchColumns: ['Data Type', 'Keywords', 'Best Chart Type', 'Accessibility Notes'],
46
+ outputColumns: [
47
+ 'Data Type',
48
+ 'Keywords',
49
+ 'Best Chart Type',
50
+ 'Secondary Options',
51
+ 'Color Guidance',
52
+ 'Accessibility Notes',
53
+ 'Library Recommendation',
54
+ 'Interactive Level',
55
+ ],
56
+ },
57
+ landing: {
58
+ file: 'landing.csv',
59
+ searchColumns: ['Pattern Name', 'Keywords', 'Conversion Optimization', 'Section Order'],
60
+ outputColumns: [
61
+ 'Pattern Name',
62
+ 'Keywords',
63
+ 'Section Order',
64
+ 'Primary CTA Placement',
65
+ 'Color Strategy',
66
+ 'Conversion Optimization',
67
+ ],
68
+ },
69
+ product: {
70
+ file: 'products.csv',
71
+ searchColumns: ['Product Type', 'Keywords', 'Primary Style Recommendation', 'Key Considerations'],
72
+ outputColumns: [
73
+ 'Product Type',
74
+ 'Keywords',
75
+ 'Primary Style Recommendation',
76
+ 'Secondary Styles',
77
+ 'Landing Page Pattern',
78
+ 'Dashboard Style (if applicable)',
79
+ 'Color Palette Focus',
80
+ ],
81
+ },
82
+ ux: {
83
+ file: 'ux-guidelines.csv',
84
+ searchColumns: ['Category', 'Issue', 'Description', 'Platform'],
85
+ outputColumns: [
86
+ 'Category',
87
+ 'Issue',
88
+ 'Platform',
89
+ 'Description',
90
+ 'Do',
91
+ "Don't",
92
+ 'Code Example Good',
93
+ 'Code Example Bad',
94
+ 'Severity',
95
+ ],
96
+ },
97
+ typography: {
98
+ file: 'typography.csv',
99
+ searchColumns: [
100
+ 'Font Pairing Name',
101
+ 'Category',
102
+ 'Mood/Style Keywords',
103
+ 'Best For',
104
+ 'Heading Font',
105
+ 'Body Font',
106
+ ],
107
+ outputColumns: [
108
+ 'Font Pairing Name',
109
+ 'Category',
110
+ 'Heading Font',
111
+ 'Body Font',
112
+ 'Mood/Style Keywords',
113
+ 'Best For',
114
+ 'Google Fonts URL',
115
+ 'CSS Import',
116
+ 'Tailwind Config',
117
+ 'Notes',
118
+ ],
119
+ },
120
+ icons: {
121
+ file: 'icons.csv',
122
+ searchColumns: ['Category', 'Icon Name', 'Keywords', 'Best For'],
123
+ outputColumns: ['Category', 'Icon Name', 'Keywords', 'Library', 'Import Code', 'Usage', 'Best For', 'Style'],
124
+ },
125
+ };
126
+
127
+ const STACK_CONFIG = {
128
+ 'html-tailwind': { file: 'stacks/html-tailwind.csv' },
129
+ react: { file: 'stacks/react.csv' },
130
+ nextjs: { file: 'stacks/nextjs.csv' },
131
+ vue: { file: 'stacks/vue.csv' },
132
+ nuxtjs: { file: 'stacks/nuxtjs.csv' },
133
+ 'nuxt-ui': { file: 'stacks/nuxt-ui.csv' },
134
+ svelte: { file: 'stacks/svelte.csv' },
135
+ swiftui: { file: 'stacks/swiftui.csv' },
136
+ 'react-native': { file: 'stacks/react-native.csv' },
137
+ flutter: { file: 'stacks/flutter.csv' },
138
+ shadcn: { file: 'stacks/shadcn.csv' },
139
+ };
140
+
141
+ const STACK_SEARCH_COLUMNS = ['Category', 'Guideline', 'Description', 'Do', "Don't"];
142
+ const STACK_OUTPUT_COLUMNS = [
143
+ 'Category',
144
+ 'Guideline',
145
+ 'Description',
146
+ 'Do',
147
+ "Don't",
148
+ 'Code Good',
149
+ 'Code Bad',
150
+ 'Severity',
151
+ 'Docs URL',
152
+ ];
153
+
154
+ const DOMAIN_KEYWORDS = {
155
+ color: ['color', 'palette', 'hex', '#', 'rgb'],
156
+ chart: [
157
+ 'chart',
158
+ 'graph',
159
+ 'visualization',
160
+ 'trend',
161
+ 'bar',
162
+ 'pie',
163
+ 'scatter',
164
+ 'heatmap',
165
+ 'funnel',
166
+ ],
167
+ landing: [
168
+ 'landing',
169
+ 'page',
170
+ 'cta',
171
+ 'conversion',
172
+ 'hero',
173
+ 'testimonial',
174
+ 'pricing',
175
+ 'section',
176
+ ],
177
+ product: [
178
+ 'saas',
179
+ 'ecommerce',
180
+ 'e-commerce',
181
+ 'fintech',
182
+ 'healthcare',
183
+ 'gaming',
184
+ 'portfolio',
185
+ 'crypto',
186
+ 'dashboard',
187
+ ],
188
+ prompt: ['prompt', 'css', 'implementation', 'variable', 'checklist', 'tailwind'],
189
+ style: [
190
+ 'style',
191
+ 'design',
192
+ 'ui',
193
+ 'minimalism',
194
+ 'glassmorphism',
195
+ 'neumorphism',
196
+ 'brutalism',
197
+ 'dark mode',
198
+ 'flat',
199
+ 'aurora',
200
+ ],
201
+ ux: [
202
+ 'ux',
203
+ 'usability',
204
+ 'accessibility',
205
+ 'wcag',
206
+ 'touch',
207
+ 'scroll',
208
+ 'animation',
209
+ 'keyboard',
210
+ 'navigation',
211
+ 'mobile',
212
+ ],
213
+ typography: ['font', 'typography', 'heading', 'serif', 'sans'],
214
+ icons: [
215
+ 'icon',
216
+ 'icons',
217
+ 'lucide',
218
+ 'heroicons',
219
+ 'symbol',
220
+ 'glyph',
221
+ 'pictogram',
222
+ 'svg icon',
223
+ ],
224
+ };
225
+
226
+ function detectDomain(query) {
227
+ const queryLower = String(query ?? '').toLowerCase();
228
+ const scores = new Map();
229
+
230
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
231
+ let score = 0;
232
+ for (const keyword of keywords) {
233
+ if (queryLower.includes(keyword)) {
234
+ score += 1;
235
+ }
236
+ }
237
+ scores.set(domain, score);
238
+ }
239
+
240
+ let bestDomain = 'style';
241
+ let bestScore = 0;
242
+ for (const [domain, score] of scores.entries()) {
243
+ if (score > bestScore) {
244
+ bestDomain = domain;
245
+ bestScore = score;
246
+ }
247
+ }
248
+
249
+ return bestScore > 0 ? bestDomain : 'style';
250
+ }
251
+
252
+ const AVAILABLE_DOMAINS = Object.keys(DOMAIN_CONFIG);
253
+ const AVAILABLE_STACKS = Object.keys(STACK_CONFIG);
254
+
255
+ module.exports = {
256
+ DOMAIN_CONFIG,
257
+ STACK_CONFIG,
258
+ STACK_SEARCH_COLUMNS,
259
+ STACK_OUTPUT_COLUMNS,
260
+ detectDomain,
261
+ AVAILABLE_DOMAINS,
262
+ AVAILABLE_STACKS,
263
+ };
264
+
@@ -0,0 +1,90 @@
1
+ const fs = require('fs');
2
+
3
+ function parseCsv(text) {
4
+ const rows = [];
5
+ let row = [];
6
+ let field = '';
7
+ let inQuotes = false;
8
+
9
+ for (let index = 0; index < text.length; index += 1) {
10
+ const char = text[index];
11
+
12
+ if (inQuotes) {
13
+ if (char === '"') {
14
+ const next = text[index + 1];
15
+ if (next === '"') {
16
+ field += '"';
17
+ index += 1;
18
+ continue;
19
+ }
20
+ inQuotes = false;
21
+ continue;
22
+ }
23
+ field += char;
24
+ continue;
25
+ }
26
+
27
+ if (char === '"') {
28
+ inQuotes = true;
29
+ continue;
30
+ }
31
+
32
+ if (char === ',') {
33
+ row.push(field);
34
+ field = '';
35
+ continue;
36
+ }
37
+
38
+ if (char === '\r') {
39
+ continue;
40
+ }
41
+
42
+ if (char === '\n') {
43
+ row.push(field);
44
+ field = '';
45
+ rows.push(row);
46
+ row = [];
47
+ continue;
48
+ }
49
+
50
+ field += char;
51
+ }
52
+
53
+ row.push(field);
54
+ rows.push(row);
55
+
56
+ return rows.filter((record) => record.some((cell) => String(cell).trim() !== ''));
57
+ }
58
+
59
+ function parseCsvFile(filePath) {
60
+ const raw = fs.readFileSync(filePath, 'utf-8');
61
+ const rows = parseCsv(raw);
62
+ if (rows.length === 0) {
63
+ return { headers: [], records: [] };
64
+ }
65
+
66
+ const headers = rows[0].map((cell, index) => {
67
+ const value = String(cell ?? '');
68
+ const cleaned = index === 0 ? value.replace(/^\uFEFF/, '') : value;
69
+ return cleaned.trim();
70
+ });
71
+
72
+ const records = rows.slice(1).map((cells) => {
73
+ const record = {};
74
+ for (let index = 0; index < headers.length; index += 1) {
75
+ const key = headers[index];
76
+ if (!key) {
77
+ continue;
78
+ }
79
+ record[key] = String(cells[index] ?? '').trim();
80
+ }
81
+ return record;
82
+ });
83
+
84
+ return { headers, records };
85
+ }
86
+
87
+ module.exports = {
88
+ parseCsvFile,
89
+ };
90
+