2024-09-11 23:10:28 +02:00
|
|
|
<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>
|
2024-09-11 23:42:08 +02:00
|
|
|
<!-- //NOSONAR --><div flex-1 break-anywhere text-wrap style="white-space: pre" v-html="highlightedText" />
|
2024-09-11 23:10:28 +02:00
|
|
|
|
|
|
|
<c-button @click="copy()">
|
|
|
|
<icon-mdi:content-copy />
|
|
|
|
</c-button>
|
|
|
|
</c-card>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="less">
|
|
|
|
.highlight {
|
|
|
|
background-color: #ff0;
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|