Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(new tool): Smart Text Replacer and LineBreaks manager #1290

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ declare module '@vue/runtime-core' {
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
Expand Down Expand Up @@ -135,14 +136,15 @@ declare module '@vue/runtime-core' {
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NFlex: typeof import('naive-ui')['NFlex']
NFormItem: typeof import('naive-ui')['NFormItem']
NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NSpace: typeof import('naive-ui')['NSpace']
NTable: typeof import('naive-ui')['NTable']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
Expand All @@ -162,6 +164,7 @@ declare module '@vue/runtime-core' {
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default']
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SmartTextReplacer: typeof import('./src/tools/smart-text-replacer/smart-text-replacer.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
Expand Down
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -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 emailNormalizer } from './email-normalizer';

import { tool as asciiTextDrawer } from './ascii-text-drawer';
Expand Down Expand Up @@ -183,6 +184,7 @@ export const toolsByCategory: ToolCategory[] = [
stringObfuscator,
textDiff,
numeronymGenerator,
smartTextReplacer,
asciiTextDrawer,
],
},
Expand Down
12 changes: 12 additions & 0 deletions src/tools/smart-text-replacer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Search } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'Smart Text Replacer and Linebreaker',
path: '/smart-text-replacer',
description: 'Search and replace a word on single or multiple occurrences just like windows notepad search and replace. Also allows to manage linebreaking and text splitting',
keywords: ['smart', 'text-replacer', 'linebreak', 'remove', 'add', 'split', 'search', 'replace'],
component: () => import('./smart-text-replacer.vue'),
icon: Search,
createdAt: new Date('2024-04-03'),
});
200 changes: 200 additions & 0 deletions src/tools/smart-text-replacer/smart-text-replacer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<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;
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="Replacement expression" @keyup.enter="replaceSelected()" />
</div>
</div>

<n-space mt-4 gap-1 align="baseline" justify="space-between">
<c-button @click="findNext()">
<label>Find Next</label>
</c-button>
<c-button @click="replaceSelected()">
<label>Replace</label>
</c-button>
<c-button @click="replaceAll()">
<label>Replace All</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>
</n-space>

<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>
<!-- //NOSONAR --><div flex-1 break-anywhere text-wrap style="white-space: pre-wrap" 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