mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-04 13:29:13 -04:00
feat: add smart text replacer, resolves #616
This commit is contained in:
parent
d3b32cc14e
commit
dcfca3f60d
7 changed files with 226 additions and 0 deletions
13
components.d.ts
vendored
13
components.d.ts
vendored
|
@ -89,7 +89,9 @@ 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']
|
||||
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
|
||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||
|
@ -126,12 +128,16 @@ declare module '@vue/runtime-core' {
|
|||
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
||||
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
|
@ -139,12 +145,17 @@ declare module '@vue/runtime-core' {
|
|||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NInputText: typeof import('naive-ui')['NInputText']
|
||||
NLabel: typeof import('naive-ui')['NLabel']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
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']
|
||||
|
@ -159,7 +170,9 @@ declare module '@vue/runtime-core' {
|
|||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
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']
|
||||
|
|
|
@ -388,6 +388,10 @@ tools:
|
|||
title: Encode/decode url formatted strings
|
||||
description: Encode to url-encoded format (also known as "percent-encoded") or decode from it.
|
||||
|
||||
smart-text-replacer:
|
||||
title: Smart text replacer
|
||||
description: Search and replace a word on single or multiple occurrences just like windows notepad search and replace.
|
||||
|
||||
text-to-binary:
|
||||
title: Text to ASCII binary
|
||||
description: Convert text to its ASCII binary representation and vice versa.
|
||||
|
|
|
@ -387,3 +387,7 @@ tools:
|
|||
text-to-binary:
|
||||
title: 文本到 ASCII 二进制
|
||||
description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。
|
||||
|
||||
smart-text-replacer:
|
||||
title: 智能文本替换器
|
||||
description: 像windows记事本一样搜索和替换单个或多个出现的单词。
|
||||
|
|
|
@ -72,6 +72,7 @@ import { tool as romanNumeralConverter } from './roman-numeral-converter';
|
|||
import { tool as sqlPrettify } from './sql-prettify';
|
||||
import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator';
|
||||
import { tool as temperatureConverter } from './temperature-converter';
|
||||
import { tool as smartTextReplacer } from './smart-text-replacer';
|
||||
import { tool as textStatistics } from './text-statistics';
|
||||
import { tool as tokenGenerator } from './token-generator';
|
||||
import type { ToolCategory } from './tools.types';
|
||||
|
@ -170,6 +171,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
emojiPicker,
|
||||
stringObfuscator,
|
||||
textDiff,
|
||||
smartTextReplacer,
|
||||
numeronymGenerator,
|
||||
asciiTextDrawer,
|
||||
],
|
||||
|
|
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', 'search', 'replace'],
|
||||
component: () => import('./smart-text-replacer.vue'),
|
||||
icon: Search,
|
||||
createdAt: new Date('2024-04-03'),
|
||||
});
|
35
src/tools/smart-text-replacer/smart-text-replacer.model.ts
Normal file
35
src/tools/smart-text-replacer/smart-text-replacer.model.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { get } from '@vueuse/core';
|
||||
import { type MaybeRef, computed } from 'vue';
|
||||
|
||||
export { obfuscateString, useObfuscateString };
|
||||
|
||||
function obfuscateString(
|
||||
str: string,
|
||||
{ replacementChar = '*', keepFirst = 4, keepLast = 0, keepSpace = true }: { replacementChar?: string; keepFirst?: number; keepLast?: number; keepSpace?: boolean } = {}): string {
|
||||
return str
|
||||
.split('')
|
||||
.map((char, index, array) => {
|
||||
if (keepSpace && char === ' ') {
|
||||
return char;
|
||||
}
|
||||
|
||||
return (index < keepFirst || index >= array.length - keepLast) ? char : replacementChar;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function useObfuscateString(
|
||||
str: MaybeRef<string>,
|
||||
config: { replacementChar?: MaybeRef<string>; keepFirst?: MaybeRef<number>; keepLast?: MaybeRef<number>; keepSpace?: MaybeRef<boolean> } = {},
|
||||
|
||||
) {
|
||||
return computed(() => obfuscateString(
|
||||
get(str),
|
||||
{
|
||||
replacementChar: get(config.replacementChar),
|
||||
keepFirst: get(config.keepFirst),
|
||||
keepLast: get(config.keepLast),
|
||||
keepSpace: get(config.keepSpace),
|
||||
},
|
||||
));
|
||||
}
|
155
src/tools/smart-text-replacer/smart-text-replacer.vue
Normal file
155
src/tools/smart-text-replacer/smart-text-replacer.vue
Normal file
|
@ -0,0 +1,155 @@
|
|||
<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);
|
||||
|
||||
// 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();
|
||||
const strValue = str.value;
|
||||
|
||||
if (!findWhatValue || !strValue) {
|
||||
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" @keyup.enter="findNext()" />
|
||||
</div>
|
||||
<div flex-1>
|
||||
<div>Replace with:</div>
|
||||
<c-input-text v-model:value="replaceWith" @keyup.enter="replaceSelected()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mt-4 w-full flex gap-10px>
|
||||
<div flex flex-1 gap-10px>
|
||||
<c-button @click="findNext()">
|
||||
<label>Find Next</label>
|
||||
</c-button>
|
||||
<n-checkbox v-model:checked="matchCase">
|
||||
<label>Match case</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>
|
||||
|
||||
<c-card v-if="highlightedText" mt-60px max-w-600px flex items-center gap-5px font-mono>
|
||||
<div flex-1 break-anywhere text-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…
Add table
Add a link
Reference in a new issue