Merge 1702fc32c8 into 07eea0f484
				
					
				
			This commit is contained in:
		
						commit
						ec4e5c9ae0
					
				
							
								
								
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -90,6 +90,7 @@ declare module '@vue/runtime-core' { | |||||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] |     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'] |     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: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'] |     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||||
|     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] |     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] |     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||||
| @ -135,6 +136,8 @@ declare module '@vue/runtime-core' { | |||||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] |     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||||
|     NDivider: typeof import('naive-ui')['NDivider'] |     NDivider: typeof import('naive-ui')['NDivider'] | ||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|  |     NFlex: typeof import('naive-ui')['NFlex'] | ||||||
|  |     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||||
|     NH1: typeof import('naive-ui')['NH1'] |     NH1: typeof import('naive-ui')['NH1'] | ||||||
|     NH3: typeof import('naive-ui')['NH3'] |     NH3: typeof import('naive-ui')['NH3'] | ||||||
|     NIcon: typeof import('naive-ui')['NIcon'] |     NIcon: typeof import('naive-ui')['NIcon'] | ||||||
| @ -142,7 +145,6 @@ declare module '@vue/runtime-core' { | |||||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] |     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||||
|     NMenu: typeof import('naive-ui')['NMenu'] |     NMenu: typeof import('naive-ui')['NMenu'] | ||||||
|     NSpace: typeof import('naive-ui')['NSpace'] |     NSpace: typeof import('naive-ui')['NSpace'] | ||||||
|     NTable: typeof import('naive-ui')['NTable'] |  | ||||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] |     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'] |     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'] |     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||||
| @ -162,6 +164,7 @@ declare module '@vue/runtime-core' { | |||||||
|     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] |     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'] |     SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] | ||||||
|     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.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'] |     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] |     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] | ||||||
|     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default'] |     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default'] | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { tool as base64FileConverter } from './base64-file-converter'; | import { tool as base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | 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 emailNormalizer } from './email-normalizer'; | ||||||
| 
 | 
 | ||||||
| import { tool as asciiTextDrawer } from './ascii-text-drawer'; | import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||||
| @ -183,6 +184,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|       stringObfuscator, |       stringObfuscator, | ||||||
|       textDiff, |       textDiff, | ||||||
|       numeronymGenerator, |       numeronymGenerator, | ||||||
|  |       smartTextReplacer, | ||||||
|       asciiTextDrawer, |       asciiTextDrawer, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								src/tools/smart-text-replacer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/smart-text-replacer/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
								
								src/tools/smart-text-replacer/smart-text-replacer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								src/tools/smart-text-replacer/smart-text-replacer.vue
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
		Reference in New Issue
	
	Block a user