mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-22 15:56:15 -04:00
feat(new tool): Smart Text Replacer and LineBreaks manager
Smart Replacer functionality taken as base from #976 by @utf26 Fixed linebreaking display in Smart Replacer Add linebreaking options Fix #1279 #1194 #616
This commit is contained in:
parent
80e46c9292
commit
2f2b3db115
3 changed files with 227 additions and 1 deletions
|
@ -1,6 +1,7 @@
|
|||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as smartTextReplacer } from './smart-text-replacer';
|
||||
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
|
@ -155,7 +156,15 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
},
|
||||
{
|
||||
name: 'Text',
|
||||
components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff, numeronymGenerator],
|
||||
components: [
|
||||
loremIpsumGenerator,
|
||||
textStatistics,
|
||||
emojiPicker,
|
||||
stringObfuscator,
|
||||
textDiff,
|
||||
numeronymGenerator,
|
||||
smartTextReplacer,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
|
|
13
src/tools/smart-text-replacer/index.ts
Normal file
13
src/tools/smart-text-replacer/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Search } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
import { translate } from '@/plugins/i18n.plugin';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: translate('tools.smart-text-replacer.title'),
|
||||
path: '/smart-text-replacer',
|
||||
description: translate('tools.smart-text-replacer.description'),
|
||||
keywords: ['smart', 'text-replacer', 'linebreak', 'remove', 'add', 'split', 'search', 'replace'],
|
||||
component: () => import('./smart-text-replacer.vue'),
|
||||
icon: Search,
|
||||
createdAt: new Date('2024-04-03'),
|
||||
});
|
204
src/tools/smart-text-replacer/smart-text-replacer.vue
Normal file
204
src/tools/smart-text-replacer/smart-text-replacer.vue
Normal file
|
@ -0,0 +1,204 @@
|
|||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const str = ref('Lorem ipsum dolor sit amet DOLOR Lorem ipsum dolor sit amet DOLOR');
|
||||
const findWhat = ref('');
|
||||
const replaceWith = ref('');
|
||||
const matchCase = ref(false);
|
||||
const keepLineBreaks = ref(true);
|
||||
const addLineBreakPlace = ref('before');
|
||||
const addLineBreakRegex = ref('');
|
||||
const splitEveryCharacterCounts = ref(0);
|
||||
|
||||
// Tracks the index of the currently active highlight.
|
||||
const currentActiveIndex = ref(0);
|
||||
// Tracks the total number of matches found to cycle through them.
|
||||
const totalMatches = ref(0);
|
||||
|
||||
const highlightedText = computed(() => {
|
||||
const findWhatValue = findWhat.value.trim();
|
||||
let strValue = str.value;
|
||||
|
||||
if (!strValue) {
|
||||
return strValue;
|
||||
}
|
||||
|
||||
if (!keepLineBreaks.value) {
|
||||
strValue = strValue.replace(/\r?\n/g, '');
|
||||
}
|
||||
|
||||
if (addLineBreakRegex.value) {
|
||||
const addLBRegex = new RegExp(addLineBreakRegex.value, matchCase.value ? 'g' : 'gi');
|
||||
if (addLineBreakPlace.value === 'before') {
|
||||
strValue = strValue.replace(addLBRegex, m => `\n${m}`);
|
||||
}
|
||||
else if (addLineBreakPlace.value === 'after') {
|
||||
strValue = strValue.replace(addLBRegex, m => `${m}\n`);
|
||||
}
|
||||
else if (addLineBreakPlace.value === 'place') {
|
||||
strValue = strValue.replace(addLBRegex, '\n');
|
||||
}
|
||||
}
|
||||
if (splitEveryCharacterCounts.value) {
|
||||
strValue = strValue.replace(new RegExp(`[^\n]{${splitEveryCharacterCounts.value}}`, 'g'), m => `${m}\n`);
|
||||
}
|
||||
|
||||
if (!findWhatValue) {
|
||||
return strValue;
|
||||
}
|
||||
|
||||
const regex = new RegExp(findWhatValue, matchCase.value ? 'g' : 'gi');
|
||||
let index = 0;
|
||||
const newStr = strValue.replace(regex, (match) => {
|
||||
index++;
|
||||
return `<span class="${match === findWhatValue ? 'highlight' : 'outline'}">${match}</span>`;
|
||||
});
|
||||
|
||||
totalMatches.value = index;
|
||||
// Reset to -1 to ensure the first match is highlighted upon next search
|
||||
currentActiveIndex.value = -1;
|
||||
return newStr;
|
||||
});
|
||||
|
||||
// Automatically highlight the first occurrence after any change
|
||||
watchEffect(async () => {
|
||||
if (highlightedText.value) {
|
||||
await nextTick();
|
||||
updateHighlighting();
|
||||
}
|
||||
});
|
||||
|
||||
watch(matchCase, () => {
|
||||
// Use nextTick to wait for the DOM to update after highlightedText re-reaction
|
||||
nextTick().then(() => {
|
||||
const matches = document.querySelectorAll('.outline, .highlight');
|
||||
if (matches.length === 0) {
|
||||
// No matches after change, reset
|
||||
currentActiveIndex.value = -1;
|
||||
totalMatches.value = 0;
|
||||
}
|
||||
else if (matches.length <= currentActiveIndex.value || currentActiveIndex.value === -1) {
|
||||
// Current selection is out of range or reset, select the first match
|
||||
currentActiveIndex.value = 0;
|
||||
updateHighlighting(); // Ensure correct highlighting
|
||||
}
|
||||
else {
|
||||
// The current selection is still valid, ensure it's highlighted correctly
|
||||
updateHighlighting(); // This might need adjustment to not advance the index
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Function to add active highlighting
|
||||
function updateHighlighting() {
|
||||
currentActiveIndex.value = (currentActiveIndex.value + 1) % totalMatches.value;
|
||||
const matches = document.querySelectorAll('.outline, .highlight');
|
||||
matches.forEach((match, index) => {
|
||||
match.className = index === currentActiveIndex.value ? 'highlight' : 'outline';
|
||||
});
|
||||
}
|
||||
|
||||
function replaceSelected() {
|
||||
const matches = document.querySelectorAll('.outline, .highlight');
|
||||
if (matches.length > currentActiveIndex.value) {
|
||||
const selectedMatch = matches[currentActiveIndex.value];
|
||||
if (selectedMatch) {
|
||||
const newText = replaceWith.value;
|
||||
selectedMatch.textContent = newText;
|
||||
selectedMatch.classList.remove('highlight');
|
||||
currentActiveIndex.value--;
|
||||
totalMatches.value--;
|
||||
}
|
||||
}
|
||||
updateHighlighting();
|
||||
}
|
||||
|
||||
function replaceAll() {
|
||||
const matches = document.querySelectorAll('.outline, .highlight');
|
||||
matches.forEach((match) => {
|
||||
match.textContent = replaceWith.value;
|
||||
match.classList.remove('highlight');
|
||||
match.classList.remove('outline');
|
||||
});
|
||||
currentActiveIndex.value = -1;
|
||||
totalMatches.value = matches.length;
|
||||
}
|
||||
|
||||
function findNext() {
|
||||
updateHighlighting();
|
||||
}
|
||||
|
||||
const { copy } = useCopy({ source: highlightedText });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text v-model:value="str" raw-text placeholder="Enter text here..." label="Text to search and replace:" clearable multiline rows="10" />
|
||||
|
||||
<div mt-4 w-full flex gap-10px>
|
||||
<div flex-1>
|
||||
<div>Find what:</div>
|
||||
<c-input-text v-model:value="findWhat" placeholder="Search regex" @keyup.enter="findNext()" />
|
||||
</div>
|
||||
<div flex-1>
|
||||
<div>Replace with:</div>
|
||||
<c-input-text v-model:value="replaceWith" placeholder="(can include $1 or $<groupName>)" @keyup.enter="replaceSelected()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mt-4 w-full flex gap-10px>
|
||||
<div flex-2 flex items-baseline gap-10px>
|
||||
<c-button @click="findNext()">
|
||||
<label>Find Next</label>
|
||||
</c-button>
|
||||
<n-checkbox v-model:checked="matchCase">
|
||||
<label>Match case</label>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="keepLineBreaks">
|
||||
<label>Keep linebreaks</label>
|
||||
</n-checkbox>
|
||||
</div>
|
||||
<div flex flex-1 justify-end gap-10px>
|
||||
<c-button @click="replaceSelected()">
|
||||
<label>Replace</label>
|
||||
</c-button>
|
||||
<c-button @click="replaceAll()">
|
||||
<label>Replace All</label>
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<div mt-4 w-full flex items-baseline gap-10px>
|
||||
<c-select
|
||||
v-model:value="addLineBreakPlace"
|
||||
:options="[{ value: 'before', label: 'Add linebreak before' }, { value: 'after', label: 'Add linebreak after' }, { value: 'place', label: 'Add linebreak in place of' }]"
|
||||
/>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="addLineBreakRegex"
|
||||
placeholder="Split text regex"
|
||||
/>
|
||||
</div>
|
||||
<div mt-4 w-full flex items-baseline gap-10px>
|
||||
<n-form-item label="Split every characters:" label-placement="left">
|
||||
<n-input-number v-model:value="splitEveryCharacterCounts" :min="0" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
<c-card v-if="highlightedText" mt-60px max-w-600px flex items-center gap-5px font-mono>
|
||||
<div flex-1 break-anywhere text-wrap style="white-space: pre" v-html="highlightedText" />
|
||||
|
||||
<c-button @click="copy()">
|
||||
<icon-mdi:content-copy />
|
||||
</c-button>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.highlight {
|
||||
background-color: #ff0;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue