Skip to content

Commit 2f2b3db

Browse files
committed
feat(new tool): Smart Text Replacer and LineBreaks manager
Smart Replacer functionality taken as base from CorentinTh#976 by @utf26 Fixed linebreaking display in Smart Replacer Add linebreaking options Fix CorentinTh#1279 CorentinTh#1194 CorentinTh#616
1 parent 80e46c9 commit 2f2b3db

File tree

3 files changed

+227
-1
lines changed

3 files changed

+227
-1
lines changed

src/tools/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
4+
import { tool as smartTextReplacer } from './smart-text-replacer';
45
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
56
import { tool as numeronymGenerator } from './numeronym-generator';
67
import { tool as macAddressGenerator } from './mac-address-generator';
@@ -155,7 +156,15 @@ export const toolsByCategory: ToolCategory[] = [
155156
},
156157
{
157158
name: 'Text',
158-
components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff, numeronymGenerator],
159+
components: [
160+
loremIpsumGenerator,
161+
textStatistics,
162+
emojiPicker,
163+
stringObfuscator,
164+
textDiff,
165+
numeronymGenerator,
166+
smartTextReplacer,
167+
],
159168
},
160169
{
161170
name: 'Data',
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Search } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
import { translate } from '@/plugins/i18n.plugin';
4+
5+
export const tool = defineTool({
6+
name: translate('tools.smart-text-replacer.title'),
7+
path: '/smart-text-replacer',
8+
description: translate('tools.smart-text-replacer.description'),
9+
keywords: ['smart', 'text-replacer', 'linebreak', 'remove', 'add', 'split', 'search', 'replace'],
10+
component: () => import('./smart-text-replacer.vue'),
11+
icon: Search,
12+
createdAt: new Date('2024-04-03'),
13+
});
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<script setup lang="ts">
2+
import { useCopy } from '@/composable/copy';
3+
4+
const str = ref('Lorem ipsum dolor sit amet DOLOR Lorem ipsum dolor sit amet DOLOR');
5+
const findWhat = ref('');
6+
const replaceWith = ref('');
7+
const matchCase = ref(false);
8+
const keepLineBreaks = ref(true);
9+
const addLineBreakPlace = ref('before');
10+
const addLineBreakRegex = ref('');
11+
const splitEveryCharacterCounts = ref(0);
12+
13+
// Tracks the index of the currently active highlight.
14+
const currentActiveIndex = ref(0);
15+
// Tracks the total number of matches found to cycle through them.
16+
const totalMatches = ref(0);
17+
18+
const highlightedText = computed(() => {
19+
const findWhatValue = findWhat.value.trim();
20+
let strValue = str.value;
21+
22+
if (!strValue) {
23+
return strValue;
24+
}
25+
26+
if (!keepLineBreaks.value) {
27+
strValue = strValue.replace(/\r?\n/g, '');
28+
}
29+
30+
if (addLineBreakRegex.value) {
31+
const addLBRegex = new RegExp(addLineBreakRegex.value, matchCase.value ? 'g' : 'gi');
32+
if (addLineBreakPlace.value === 'before') {
33+
strValue = strValue.replace(addLBRegex, m => `\n${m}`);
34+
}
35+
else if (addLineBreakPlace.value === 'after') {
36+
strValue = strValue.replace(addLBRegex, m => `${m}\n`);
37+
}
38+
else if (addLineBreakPlace.value === 'place') {
39+
strValue = strValue.replace(addLBRegex, '\n');
40+
}
41+
}
42+
if (splitEveryCharacterCounts.value) {
43+
strValue = strValue.replace(new RegExp(`[^\n]{${splitEveryCharacterCounts.value}}`, 'g'), m => `${m}\n`);
44+
}
45+
46+
if (!findWhatValue) {
47+
return strValue;
48+
}
49+
50+
const regex = new RegExp(findWhatValue, matchCase.value ? 'g' : 'gi');
51+
let index = 0;
52+
const newStr = strValue.replace(regex, (match) => {
53+
index++;
54+
return `<span class="${match === findWhatValue ? 'highlight' : 'outline'}">${match}</span>`;
55+
});
56+
57+
totalMatches.value = index;
58+
// Reset to -1 to ensure the first match is highlighted upon next search
59+
currentActiveIndex.value = -1;
60+
return newStr;
61+
});
62+
63+
// Automatically highlight the first occurrence after any change
64+
watchEffect(async () => {
65+
if (highlightedText.value) {
66+
await nextTick();
67+
updateHighlighting();
68+
}
69+
});
70+
71+
watch(matchCase, () => {
72+
// Use nextTick to wait for the DOM to update after highlightedText re-reaction
73+
nextTick().then(() => {
74+
const matches = document.querySelectorAll('.outline, .highlight');
75+
if (matches.length === 0) {
76+
// No matches after change, reset
77+
currentActiveIndex.value = -1;
78+
totalMatches.value = 0;
79+
}
80+
else if (matches.length <= currentActiveIndex.value || currentActiveIndex.value === -1) {
81+
// Current selection is out of range or reset, select the first match
82+
currentActiveIndex.value = 0;
83+
updateHighlighting(); // Ensure correct highlighting
84+
}
85+
else {
86+
// The current selection is still valid, ensure it's highlighted correctly
87+
updateHighlighting(); // This might need adjustment to not advance the index
88+
}
89+
});
90+
});
91+
92+
// Function to add active highlighting
93+
function updateHighlighting() {
94+
currentActiveIndex.value = (currentActiveIndex.value + 1) % totalMatches.value;
95+
const matches = document.querySelectorAll('.outline, .highlight');
96+
matches.forEach((match, index) => {
97+
match.className = index === currentActiveIndex.value ? 'highlight' : 'outline';
98+
});
99+
}
100+
101+
function replaceSelected() {
102+
const matches = document.querySelectorAll('.outline, .highlight');
103+
if (matches.length > currentActiveIndex.value) {
104+
const selectedMatch = matches[currentActiveIndex.value];
105+
if (selectedMatch) {
106+
const newText = replaceWith.value;
107+
selectedMatch.textContent = newText;
108+
selectedMatch.classList.remove('highlight');
109+
currentActiveIndex.value--;
110+
totalMatches.value--;
111+
}
112+
}
113+
updateHighlighting();
114+
}
115+
116+
function replaceAll() {
117+
const matches = document.querySelectorAll('.outline, .highlight');
118+
matches.forEach((match) => {
119+
match.textContent = replaceWith.value;
120+
match.classList.remove('highlight');
121+
match.classList.remove('outline');
122+
});
123+
currentActiveIndex.value = -1;
124+
totalMatches.value = matches.length;
125+
}
126+
127+
function findNext() {
128+
updateHighlighting();
129+
}
130+
131+
const { copy } = useCopy({ source: highlightedText });
132+
</script>
133+
134+
<template>
135+
<div>
136+
<c-input-text v-model:value="str" raw-text placeholder="Enter text here..." label="Text to search and replace:" clearable multiline rows="10" />
137+
138+
<div mt-4 w-full flex gap-10px>
139+
<div flex-1>
140+
<div>Find what:</div>
141+
<c-input-text v-model:value="findWhat" placeholder="Search regex" @keyup.enter="findNext()" />
142+
</div>
143+
<div flex-1>
144+
<div>Replace with:</div>
145+
<c-input-text v-model:value="replaceWith" placeholder="(can include $1 or $<groupName>)" @keyup.enter="replaceSelected()" />
146+
</div>
147+
</div>
148+
149+
<div mt-4 w-full flex gap-10px>
150+
<div flex-2 flex items-baseline gap-10px>
151+
<c-button @click="findNext()">
152+
<label>Find Next</label>
153+
</c-button>
154+
<n-checkbox v-model:checked="matchCase">
155+
<label>Match case</label>
156+
</n-checkbox>
157+
<n-checkbox v-model:checked="keepLineBreaks">
158+
<label>Keep linebreaks</label>
159+
</n-checkbox>
160+
</div>
161+
<div flex flex-1 justify-end gap-10px>
162+
<c-button @click="replaceSelected()">
163+
<label>Replace</label>
164+
</c-button>
165+
<c-button @click="replaceAll()">
166+
<label>Replace All</label>
167+
</c-button>
168+
</div>
169+
</div>
170+
171+
<n-divider />
172+
173+
<div mt-4 w-full flex items-baseline gap-10px>
174+
<c-select
175+
v-model:value="addLineBreakPlace"
176+
:options="[{ value: 'before', label: 'Add linebreak before' }, { value: 'after', label: 'Add linebreak after' }, { value: 'place', label: 'Add linebreak in place of' }]"
177+
/>
178+
179+
<c-input-text
180+
v-model:value="addLineBreakRegex"
181+
placeholder="Split text regex"
182+
/>
183+
</div>
184+
<div mt-4 w-full flex items-baseline gap-10px>
185+
<n-form-item label="Split every characters:" label-placement="left">
186+
<n-input-number v-model:value="splitEveryCharacterCounts" :min="0" />
187+
</n-form-item>
188+
</div>
189+
<c-card v-if="highlightedText" mt-60px max-w-600px flex items-center gap-5px font-mono>
190+
<div flex-1 break-anywhere text-wrap style="white-space: pre" v-html="highlightedText" />
191+
192+
<c-button @click="copy()">
193+
<icon-mdi:content-copy />
194+
</c-button>
195+
</c-card>
196+
</div>
197+
</template>
198+
199+
<style lang="less">
200+
.highlight {
201+
background-color: #ff0;
202+
color: black;
203+
}
204+
</style>

0 commit comments

Comments
 (0)