@rog0x/mcp-string-tools 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/README.md +73 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +153 -0
- package/dist/tools/slug-generator.d.ts +14 -0
- package/dist/tools/slug-generator.js +85 -0
- package/dist/tools/string-analyzer.d.ts +21 -0
- package/dist/tools/string-analyzer.js +109 -0
- package/dist/tools/string-diff.d.ts +20 -0
- package/dist/tools/string-diff.js +130 -0
- package/dist/tools/string-transformer.d.ts +16 -0
- package/dist/tools/string-transformer.js +177 -0
- package/dist/tools/template-engine.d.ts +8 -0
- package/dist/tools/template-engine.js +109 -0
- package/package.json +20 -0
- package/src/index.ts +186 -0
- package/src/tools/slug-generator.ts +108 -0
- package/src/tools/string-analyzer.ts +135 -0
- package/src/tools/string-diff.ts +163 -0
- package/src/tools/string-transformer.ts +227 -0
- package/src/tools/template-engine.ts +149 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export interface AnalysisResult {
|
|
2
|
+
charCount: number;
|
|
3
|
+
charCountNoSpaces: number;
|
|
4
|
+
wordCount: number;
|
|
5
|
+
sentenceCount: number;
|
|
6
|
+
paragraphCount: number;
|
|
7
|
+
readingTimeMinutes: number;
|
|
8
|
+
readingLevel: {
|
|
9
|
+
score: number;
|
|
10
|
+
grade: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
uniqueWordCount: number;
|
|
14
|
+
mostCommonWords: Array<{ word: string; count: number }>;
|
|
15
|
+
longestWord: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function countSyllables(word: string): number {
|
|
19
|
+
const w = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
20
|
+
if (w.length <= 2) return 1;
|
|
21
|
+
|
|
22
|
+
let count = 0;
|
|
23
|
+
const vowels = "aeiouy";
|
|
24
|
+
let prevVowel = false;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < w.length; i++) {
|
|
27
|
+
const isVowel = vowels.includes(w[i]);
|
|
28
|
+
if (isVowel && !prevVowel) {
|
|
29
|
+
count++;
|
|
30
|
+
}
|
|
31
|
+
prevVowel = isVowel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Silent e
|
|
35
|
+
if (w.endsWith("e") && count > 1) {
|
|
36
|
+
count--;
|
|
37
|
+
}
|
|
38
|
+
// -le ending
|
|
39
|
+
if (w.endsWith("le") && w.length > 2 && !vowels.includes(w[w.length - 3])) {
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Math.max(count, 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function analyzeString(text: string): AnalysisResult {
|
|
47
|
+
const charCount = text.length;
|
|
48
|
+
const charCountNoSpaces = text.replace(/\s/g, "").length;
|
|
49
|
+
|
|
50
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
51
|
+
const wordCount = words.length;
|
|
52
|
+
|
|
53
|
+
const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
54
|
+
const sentenceCount = sentences.length;
|
|
55
|
+
|
|
56
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
57
|
+
const paragraphCount = Math.max(paragraphs.length, text.trim().length > 0 ? 1 : 0);
|
|
58
|
+
|
|
59
|
+
const readingTimeMinutes = Math.max(Math.ceil(wordCount / 200), wordCount > 0 ? 1 : 0);
|
|
60
|
+
|
|
61
|
+
// Flesch-Kincaid
|
|
62
|
+
const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
|
|
63
|
+
let fkScore = 0;
|
|
64
|
+
let fkGrade = "N/A";
|
|
65
|
+
let fkDescription = "Text too short to analyze";
|
|
66
|
+
|
|
67
|
+
if (wordCount > 0 && sentenceCount > 0) {
|
|
68
|
+
fkScore =
|
|
69
|
+
206.835 -
|
|
70
|
+
1.015 * (wordCount / sentenceCount) -
|
|
71
|
+
84.6 * (totalSyllables / wordCount);
|
|
72
|
+
fkScore = Math.round(fkScore * 10) / 10;
|
|
73
|
+
|
|
74
|
+
if (fkScore >= 90) {
|
|
75
|
+
fkGrade = "5th grade";
|
|
76
|
+
fkDescription = "Very easy to read";
|
|
77
|
+
} else if (fkScore >= 80) {
|
|
78
|
+
fkGrade = "6th grade";
|
|
79
|
+
fkDescription = "Easy to read";
|
|
80
|
+
} else if (fkScore >= 70) {
|
|
81
|
+
fkGrade = "7th grade";
|
|
82
|
+
fkDescription = "Fairly easy to read";
|
|
83
|
+
} else if (fkScore >= 60) {
|
|
84
|
+
fkGrade = "8th-9th grade";
|
|
85
|
+
fkDescription = "Plain English";
|
|
86
|
+
} else if (fkScore >= 50) {
|
|
87
|
+
fkGrade = "10th-12th grade";
|
|
88
|
+
fkDescription = "Fairly difficult to read";
|
|
89
|
+
} else if (fkScore >= 30) {
|
|
90
|
+
fkGrade = "College";
|
|
91
|
+
fkDescription = "Difficult to read";
|
|
92
|
+
} else {
|
|
93
|
+
fkGrade = "College graduate";
|
|
94
|
+
fkDescription = "Very difficult to read";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Word frequency
|
|
99
|
+
const wordFreq = new Map<string, number>();
|
|
100
|
+
for (const w of words) {
|
|
101
|
+
const lower = w.toLowerCase().replace(/[^a-z0-9'-]/g, "");
|
|
102
|
+
if (lower.length > 0) {
|
|
103
|
+
wordFreq.set(lower, (wordFreq.get(lower) || 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const uniqueWordCount = wordFreq.size;
|
|
108
|
+
|
|
109
|
+
const mostCommonWords = Array.from(wordFreq.entries())
|
|
110
|
+
.sort((a, b) => b[1] - a[1])
|
|
111
|
+
.slice(0, 10)
|
|
112
|
+
.map(([word, count]) => ({ word, count }));
|
|
113
|
+
|
|
114
|
+
const longestWord = words.reduce(
|
|
115
|
+
(longest, w) => (w.length > longest.length ? w : longest),
|
|
116
|
+
""
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
charCount,
|
|
121
|
+
charCountNoSpaces,
|
|
122
|
+
wordCount,
|
|
123
|
+
sentenceCount,
|
|
124
|
+
paragraphCount,
|
|
125
|
+
readingTimeMinutes,
|
|
126
|
+
readingLevel: {
|
|
127
|
+
score: fkScore,
|
|
128
|
+
grade: fkGrade,
|
|
129
|
+
description: fkDescription,
|
|
130
|
+
},
|
|
131
|
+
uniqueWordCount,
|
|
132
|
+
mostCommonWords,
|
|
133
|
+
longestWord,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export type DiffType = "addition" | "deletion" | "unchanged";
|
|
2
|
+
|
|
3
|
+
export interface DiffSegment {
|
|
4
|
+
type: DiffType;
|
|
5
|
+
value: string;
|
|
6
|
+
position: { start: number; end: number };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DiffResult {
|
|
10
|
+
mode: "character" | "word" | "line";
|
|
11
|
+
segments: DiffSegment[];
|
|
12
|
+
summary: {
|
|
13
|
+
additions: number;
|
|
14
|
+
deletions: number;
|
|
15
|
+
unchanged: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function lcs<T>(a: T[], b: T[]): T[] {
|
|
20
|
+
const m = a.length;
|
|
21
|
+
const n = b.length;
|
|
22
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
23
|
+
Array(n + 1).fill(0)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (let i = 1; i <= m; i++) {
|
|
27
|
+
for (let j = 1; j <= n; j++) {
|
|
28
|
+
if (a[i - 1] === b[j - 1]) {
|
|
29
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
30
|
+
} else {
|
|
31
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result: T[] = [];
|
|
37
|
+
let i = m;
|
|
38
|
+
let j = n;
|
|
39
|
+
while (i > 0 && j > 0) {
|
|
40
|
+
if (a[i - 1] === b[j - 1]) {
|
|
41
|
+
result.unshift(a[i - 1]);
|
|
42
|
+
i--;
|
|
43
|
+
j--;
|
|
44
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
45
|
+
i--;
|
|
46
|
+
} else {
|
|
47
|
+
j--;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function computeDiff(oldTokens: string[], newTokens: string[]): DiffSegment[] {
|
|
55
|
+
const common = lcs(oldTokens, newTokens);
|
|
56
|
+
const segments: DiffSegment[] = [];
|
|
57
|
+
|
|
58
|
+
let oi = 0;
|
|
59
|
+
let ni = 0;
|
|
60
|
+
let ci = 0;
|
|
61
|
+
let pos = 0;
|
|
62
|
+
|
|
63
|
+
while (ci < common.length) {
|
|
64
|
+
// Deletions from old
|
|
65
|
+
while (oi < oldTokens.length && oldTokens[oi] !== common[ci]) {
|
|
66
|
+
const start = pos;
|
|
67
|
+
const value = oldTokens[oi];
|
|
68
|
+
segments.push({
|
|
69
|
+
type: "deletion",
|
|
70
|
+
value,
|
|
71
|
+
position: { start, end: start + value.length },
|
|
72
|
+
});
|
|
73
|
+
oi++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Additions from new
|
|
77
|
+
while (ni < newTokens.length && newTokens[ni] !== common[ci]) {
|
|
78
|
+
const start = pos;
|
|
79
|
+
const value = newTokens[ni];
|
|
80
|
+
segments.push({
|
|
81
|
+
type: "addition",
|
|
82
|
+
value,
|
|
83
|
+
position: { start, end: start + value.length },
|
|
84
|
+
});
|
|
85
|
+
pos += value.length;
|
|
86
|
+
ni++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unchanged
|
|
90
|
+
if (ci < common.length) {
|
|
91
|
+
const value = common[ci];
|
|
92
|
+
const start = pos;
|
|
93
|
+
segments.push({
|
|
94
|
+
type: "unchanged",
|
|
95
|
+
value,
|
|
96
|
+
position: { start, end: start + value.length },
|
|
97
|
+
});
|
|
98
|
+
pos += value.length;
|
|
99
|
+
oi++;
|
|
100
|
+
ni++;
|
|
101
|
+
ci++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remaining deletions
|
|
106
|
+
while (oi < oldTokens.length) {
|
|
107
|
+
const value = oldTokens[oi];
|
|
108
|
+
segments.push({
|
|
109
|
+
type: "deletion",
|
|
110
|
+
value,
|
|
111
|
+
position: { start: pos, end: pos + value.length },
|
|
112
|
+
});
|
|
113
|
+
oi++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remaining additions
|
|
117
|
+
while (ni < newTokens.length) {
|
|
118
|
+
const value = newTokens[ni];
|
|
119
|
+
segments.push({
|
|
120
|
+
type: "addition",
|
|
121
|
+
value,
|
|
122
|
+
position: { start: pos, end: pos + value.length },
|
|
123
|
+
});
|
|
124
|
+
pos += value.length;
|
|
125
|
+
ni++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return segments;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function diffStrings(
|
|
132
|
+
oldStr: string,
|
|
133
|
+
newStr: string,
|
|
134
|
+
mode: "character" | "word" | "line" = "word"
|
|
135
|
+
): DiffResult {
|
|
136
|
+
let oldTokens: string[];
|
|
137
|
+
let newTokens: string[];
|
|
138
|
+
|
|
139
|
+
switch (mode) {
|
|
140
|
+
case "character":
|
|
141
|
+
oldTokens = oldStr.split("");
|
|
142
|
+
newTokens = newStr.split("");
|
|
143
|
+
break;
|
|
144
|
+
case "word":
|
|
145
|
+
oldTokens = oldStr.split(/(\s+)/).filter((t) => t.length > 0);
|
|
146
|
+
newTokens = newStr.split(/(\s+)/).filter((t) => t.length > 0);
|
|
147
|
+
break;
|
|
148
|
+
case "line":
|
|
149
|
+
oldTokens = oldStr.split("\n");
|
|
150
|
+
newTokens = newStr.split("\n");
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const segments = computeDiff(oldTokens, newTokens);
|
|
155
|
+
|
|
156
|
+
const summary = {
|
|
157
|
+
additions: segments.filter((s) => s.type === "addition").length,
|
|
158
|
+
deletions: segments.filter((s) => s.type === "deletion").length,
|
|
159
|
+
unchanged: segments.filter((s) => s.type === "unchanged").length,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return { mode, segments, summary };
|
|
163
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export interface TransformOptions {
|
|
2
|
+
operation: string;
|
|
3
|
+
text: string;
|
|
4
|
+
maskChar?: string;
|
|
5
|
+
maxLength?: number;
|
|
6
|
+
wrapWidth?: number;
|
|
7
|
+
ellipsis?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TransformResult {
|
|
11
|
+
original: string;
|
|
12
|
+
transformed: string;
|
|
13
|
+
operation: string;
|
|
14
|
+
details?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function removeDuplicateLines(text: string): TransformResult {
|
|
18
|
+
const lines = text.split("\n");
|
|
19
|
+
const seen = new Set<string>();
|
|
20
|
+
const unique: string[] = [];
|
|
21
|
+
let removed = 0;
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (!seen.has(line)) {
|
|
25
|
+
seen.add(line);
|
|
26
|
+
unique.push(line);
|
|
27
|
+
} else {
|
|
28
|
+
removed++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
original: text,
|
|
34
|
+
transformed: unique.join("\n"),
|
|
35
|
+
operation: "remove_duplicate_lines",
|
|
36
|
+
details: { linesRemoved: removed, linesRemaining: unique.length },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeExtraWhitespace(text: string): TransformResult {
|
|
41
|
+
const transformed = text
|
|
42
|
+
.replace(/[^\S\n]+/g, " ")
|
|
43
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
44
|
+
.replace(/^ +| +$/gm, "")
|
|
45
|
+
.trim();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
original: text,
|
|
49
|
+
transformed,
|
|
50
|
+
operation: "remove_extra_whitespace",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractEmails(text: string): TransformResult {
|
|
55
|
+
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
56
|
+
const matches = text.match(emailRegex) || [];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
original: text,
|
|
60
|
+
transformed: matches.join("\n"),
|
|
61
|
+
operation: "extract_emails",
|
|
62
|
+
details: { count: matches.length, emails: matches },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractUrls(text: string): TransformResult {
|
|
67
|
+
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/g;
|
|
68
|
+
const matches = text.match(urlRegex) || [];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
original: text,
|
|
72
|
+
transformed: matches.join("\n"),
|
|
73
|
+
operation: "extract_urls",
|
|
74
|
+
details: { count: matches.length, urls: matches },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractPhoneNumbers(text: string): TransformResult {
|
|
79
|
+
const phoneRegex =
|
|
80
|
+
/(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}/g;
|
|
81
|
+
const matches = text.match(phoneRegex) || [];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
original: text,
|
|
85
|
+
transformed: matches.join("\n"),
|
|
86
|
+
operation: "extract_phone_numbers",
|
|
87
|
+
details: { count: matches.length, phoneNumbers: matches },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function maskSensitiveData(text: string, maskChar: string = "*"): TransformResult {
|
|
92
|
+
let transformed = text;
|
|
93
|
+
let maskedCount = 0;
|
|
94
|
+
|
|
95
|
+
// Mask emails: keep first 2 chars + domain
|
|
96
|
+
transformed = transformed.replace(
|
|
97
|
+
/([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
|
|
98
|
+
(_match, local: string, domain: string) => {
|
|
99
|
+
maskedCount++;
|
|
100
|
+
const keep = local.slice(0, 2);
|
|
101
|
+
return keep + maskChar.repeat(Math.max(local.length - 2, 3)) + "@" + domain;
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Mask phone numbers: keep last 4
|
|
106
|
+
transformed = transformed.replace(
|
|
107
|
+
/(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}/g,
|
|
108
|
+
(match) => {
|
|
109
|
+
maskedCount++;
|
|
110
|
+
const digits = match.replace(/\D/g, "");
|
|
111
|
+
const last4 = digits.slice(-4);
|
|
112
|
+
return maskChar.repeat(digits.length - 4) + last4;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Mask credit card numbers: keep last 4
|
|
117
|
+
transformed = transformed.replace(
|
|
118
|
+
/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
|
|
119
|
+
(match) => {
|
|
120
|
+
maskedCount++;
|
|
121
|
+
const digits = match.replace(/\D/g, "");
|
|
122
|
+
const last4 = digits.slice(-4);
|
|
123
|
+
return maskChar.repeat(12) + last4;
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
original: text,
|
|
129
|
+
transformed,
|
|
130
|
+
operation: "mask_sensitive_data",
|
|
131
|
+
details: { maskedCount },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function truncateWithEllipsis(
|
|
136
|
+
text: string,
|
|
137
|
+
maxLength: number = 100,
|
|
138
|
+
ellipsis: string = "..."
|
|
139
|
+
): TransformResult {
|
|
140
|
+
if (text.length <= maxLength) {
|
|
141
|
+
return {
|
|
142
|
+
original: text,
|
|
143
|
+
transformed: text,
|
|
144
|
+
operation: "truncate",
|
|
145
|
+
details: { truncated: false },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const cutoff = maxLength - ellipsis.length;
|
|
150
|
+
// Try to cut at word boundary
|
|
151
|
+
const lastSpace = text.lastIndexOf(" ", cutoff);
|
|
152
|
+
const breakAt = lastSpace > cutoff * 0.7 ? lastSpace : cutoff;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
original: text,
|
|
156
|
+
transformed: text.slice(0, breakAt).trimEnd() + ellipsis,
|
|
157
|
+
operation: "truncate",
|
|
158
|
+
details: {
|
|
159
|
+
truncated: true,
|
|
160
|
+
originalLength: text.length,
|
|
161
|
+
newLength: breakAt + ellipsis.length,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function wrapText(text: string, width: number = 80): TransformResult {
|
|
167
|
+
const lines = text.split("\n");
|
|
168
|
+
const wrapped: string[] = [];
|
|
169
|
+
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
if (line.length <= width) {
|
|
172
|
+
wrapped.push(line);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const words = line.split(" ");
|
|
177
|
+
let current = "";
|
|
178
|
+
|
|
179
|
+
for (const word of words) {
|
|
180
|
+
if (current.length === 0) {
|
|
181
|
+
current = word;
|
|
182
|
+
} else if (current.length + 1 + word.length <= width) {
|
|
183
|
+
current += " " + word;
|
|
184
|
+
} else {
|
|
185
|
+
wrapped.push(current);
|
|
186
|
+
current = word;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (current.length > 0) {
|
|
190
|
+
wrapped.push(current);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
original: text,
|
|
196
|
+
transformed: wrapped.join("\n"),
|
|
197
|
+
operation: "wrap_text",
|
|
198
|
+
details: { width, lineCount: wrapped.length },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function transformString(options: TransformOptions): TransformResult {
|
|
203
|
+
const { operation, text, maskChar, maxLength, wrapWidth, ellipsis } = options;
|
|
204
|
+
|
|
205
|
+
switch (operation) {
|
|
206
|
+
case "remove_duplicate_lines":
|
|
207
|
+
return removeDuplicateLines(text);
|
|
208
|
+
case "remove_extra_whitespace":
|
|
209
|
+
return removeExtraWhitespace(text);
|
|
210
|
+
case "extract_emails":
|
|
211
|
+
return extractEmails(text);
|
|
212
|
+
case "extract_urls":
|
|
213
|
+
return extractUrls(text);
|
|
214
|
+
case "extract_phone_numbers":
|
|
215
|
+
return extractPhoneNumbers(text);
|
|
216
|
+
case "mask_sensitive_data":
|
|
217
|
+
return maskSensitiveData(text, maskChar);
|
|
218
|
+
case "truncate":
|
|
219
|
+
return truncateWithEllipsis(text, maxLength, ellipsis);
|
|
220
|
+
case "wrap_text":
|
|
221
|
+
return wrapText(text, wrapWidth);
|
|
222
|
+
default:
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Unknown operation: ${operation}. Available: remove_duplicate_lines, remove_extra_whitespace, extract_emails, extract_urls, extract_phone_numbers, mask_sensitive_data, truncate, wrap_text`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export interface TemplateResult {
|
|
2
|
+
template: string;
|
|
3
|
+
rendered: string;
|
|
4
|
+
variablesUsed: string[];
|
|
5
|
+
variablesMissing: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
9
|
+
const parts = path.split(".");
|
|
10
|
+
let current: unknown = obj;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
current = (current as Record<string, unknown>)[part];
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isTruthy(value: unknown): boolean {
|
|
21
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
22
|
+
if (typeof value === "object" && value !== null) return Object.keys(value).length > 0;
|
|
23
|
+
return Boolean(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function processConditionals(
|
|
27
|
+
template: string,
|
|
28
|
+
data: Record<string, unknown>
|
|
29
|
+
): string {
|
|
30
|
+
// {{#if varName}}...content...{{/if}}
|
|
31
|
+
// {{#if varName}}...content...{{#else}}...alt...{{/if}}
|
|
32
|
+
const ifRegex = /\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
33
|
+
|
|
34
|
+
let result = template;
|
|
35
|
+
let prevResult = "";
|
|
36
|
+
|
|
37
|
+
// Iterate until stable (handles nested conditionals)
|
|
38
|
+
while (result !== prevResult) {
|
|
39
|
+
prevResult = result;
|
|
40
|
+
result = result.replace(ifRegex, (_match, varName: string, body: string) => {
|
|
41
|
+
const trimmedVar = varName.trim();
|
|
42
|
+
const value = getNestedValue(data, trimmedVar);
|
|
43
|
+
|
|
44
|
+
const elseParts = body.split(/\{\{#else\}\}/);
|
|
45
|
+
const trueBranch = elseParts[0];
|
|
46
|
+
const falseBranch = elseParts.length > 1 ? elseParts[1] : "";
|
|
47
|
+
|
|
48
|
+
return isTruthy(value) ? trueBranch : falseBranch;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function processLoops(
|
|
56
|
+
template: string,
|
|
57
|
+
data: Record<string, unknown>
|
|
58
|
+
): string {
|
|
59
|
+
// {{#each items}}...{{this}}...{{@index}}...{{/each}}
|
|
60
|
+
const eachRegex = /\{\{#each\s+([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
|
61
|
+
|
|
62
|
+
let result = template;
|
|
63
|
+
let prevResult = "";
|
|
64
|
+
|
|
65
|
+
while (result !== prevResult) {
|
|
66
|
+
prevResult = result;
|
|
67
|
+
result = result.replace(eachRegex, (_match, varName: string, body: string) => {
|
|
68
|
+
const trimmedVar = varName.trim();
|
|
69
|
+
const value = getNestedValue(data, trimmedVar);
|
|
70
|
+
|
|
71
|
+
if (!Array.isArray(value)) {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return value
|
|
76
|
+
.map((item: unknown, index: number) => {
|
|
77
|
+
let rendered = body;
|
|
78
|
+
// Replace {{this}} with current item
|
|
79
|
+
rendered = rendered.replace(/\{\{this\}\}/g, String(item));
|
|
80
|
+
// Replace {{@index}} with current index
|
|
81
|
+
rendered = rendered.replace(/\{\{@index\}\}/g, String(index));
|
|
82
|
+
|
|
83
|
+
// If item is object, allow {{this.prop}}
|
|
84
|
+
if (typeof item === "object" && item !== null) {
|
|
85
|
+
rendered = rendered.replace(
|
|
86
|
+
/\{\{this\.([^}]+)\}\}/g,
|
|
87
|
+
(_m: string, prop: string) => {
|
|
88
|
+
const val = (item as Record<string, unknown>)[prop.trim()];
|
|
89
|
+
return val !== undefined ? String(val) : "";
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return rendered;
|
|
95
|
+
})
|
|
96
|
+
.join("");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function processVariables(
|
|
104
|
+
template: string,
|
|
105
|
+
data: Record<string, unknown>
|
|
106
|
+
): { rendered: string; used: string[]; missing: string[] } {
|
|
107
|
+
const used: string[] = [];
|
|
108
|
+
const missing: string[] = [];
|
|
109
|
+
|
|
110
|
+
const rendered = template.replace(
|
|
111
|
+
/\{\{([^#/][^}]*)\}\}/g,
|
|
112
|
+
(_match, varName: string) => {
|
|
113
|
+
const trimmed = varName.trim();
|
|
114
|
+
|
|
115
|
+
// Skip special variables
|
|
116
|
+
if (trimmed === "this" || trimmed.startsWith("@") || trimmed.startsWith("this.")) {
|
|
117
|
+
return _match;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const value = getNestedValue(data, trimmed);
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
used.push(trimmed);
|
|
123
|
+
return String(value);
|
|
124
|
+
} else {
|
|
125
|
+
missing.push(trimmed);
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { rendered, used, missing };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderTemplate(
|
|
135
|
+
template: string,
|
|
136
|
+
data: Record<string, unknown>
|
|
137
|
+
): TemplateResult {
|
|
138
|
+
// Process in order: conditionals, loops, then variables
|
|
139
|
+
let result = processConditionals(template, data);
|
|
140
|
+
result = processLoops(result, data);
|
|
141
|
+
const { rendered, used, missing } = processVariables(result, data);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
template,
|
|
145
|
+
rendered,
|
|
146
|
+
variablesUsed: [...new Set(used)],
|
|
147
|
+
variablesMissing: [...new Set(missing)],
|
|
148
|
+
};
|
|
149
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|