@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.
@@ -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
+ }