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
					
				| @ -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…
	
		Reference in New Issue
	
	Block a user