feat(Text to Unicode/Binary): encoding enhancements
Fix part of #1447 and #1072
This commit is contained in:
		
							parent
							
								
									08d977b8cd
								
							
						
					
					
						commit
						97e8ff6c5c
					
				| @ -286,6 +286,9 @@ | ||||
|     "watchTriggerable": true, | ||||
|     "watchWithFilter": true, | ||||
|     "whenever": true, | ||||
|     "toValue": true | ||||
|     "toValue": true, | ||||
|     "injectLocal": true, | ||||
|     "provideLocal": true, | ||||
|     "useClipboardItems": true | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -36,6 +36,7 @@ declare global { | ||||
|   const h: typeof import('vue')['h'] | ||||
|   const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] | ||||
|   const inject: typeof import('vue')['inject'] | ||||
|   const injectLocal: typeof import('@vueuse/core')['injectLocal'] | ||||
|   const isDefined: typeof import('@vueuse/core')['isDefined'] | ||||
|   const isProxy: typeof import('vue')['isProxy'] | ||||
|   const isReactive: typeof import('vue')['isReactive'] | ||||
| @ -65,6 +66,7 @@ declare global { | ||||
|   const onUpdated: typeof import('vue')['onUpdated'] | ||||
|   const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] | ||||
|   const provide: typeof import('vue')['provide'] | ||||
|   const provideLocal: typeof import('@vueuse/core')['provideLocal'] | ||||
|   const reactify: typeof import('@vueuse/core')['reactify'] | ||||
|   const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] | ||||
|   const reactive: typeof import('vue')['reactive'] | ||||
| @ -128,6 +130,7 @@ declare global { | ||||
|   const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] | ||||
|   const useCached: typeof import('@vueuse/core')['useCached'] | ||||
|   const useClipboard: typeof import('@vueuse/core')['useClipboard'] | ||||
|   const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] | ||||
|   const useCloned: typeof import('@vueuse/core')['useCloned'] | ||||
|   const useColorMode: typeof import('@vueuse/core')['useColorMode'] | ||||
|   const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] | ||||
| @ -326,6 +329,7 @@ declare module 'vue' { | ||||
|     readonly h: UnwrapRef<typeof import('vue')['h']> | ||||
|     readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> | ||||
|     readonly inject: UnwrapRef<typeof import('vue')['inject']> | ||||
|     readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> | ||||
|     readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> | ||||
|     readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> | ||||
|     readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> | ||||
| @ -355,6 +359,7 @@ declare module 'vue' { | ||||
|     readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> | ||||
|     readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> | ||||
|     readonly provide: UnwrapRef<typeof import('vue')['provide']> | ||||
|     readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> | ||||
|     readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> | ||||
|     readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> | ||||
|     readonly reactive: UnwrapRef<typeof import('vue')['reactive']> | ||||
| @ -418,6 +423,7 @@ declare module 'vue' { | ||||
|     readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']> | ||||
|     readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']> | ||||
|     readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']> | ||||
|     readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']> | ||||
|     readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> | ||||
|     readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> | ||||
|     readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> | ||||
| @ -610,6 +616,7 @@ declare module '@vue/runtime-core' { | ||||
|     readonly h: UnwrapRef<typeof import('vue')['h']> | ||||
|     readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> | ||||
|     readonly inject: UnwrapRef<typeof import('vue')['inject']> | ||||
|     readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> | ||||
|     readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> | ||||
|     readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> | ||||
|     readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> | ||||
| @ -639,6 +646,7 @@ declare module '@vue/runtime-core' { | ||||
|     readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> | ||||
|     readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> | ||||
|     readonly provide: UnwrapRef<typeof import('vue')['provide']> | ||||
|     readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> | ||||
|     readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> | ||||
|     readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> | ||||
|     readonly reactive: UnwrapRef<typeof import('vue')['reactive']> | ||||
| @ -702,6 +710,7 @@ declare module '@vue/runtime-core' { | ||||
|     readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']> | ||||
|     readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']> | ||||
|     readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']> | ||||
|     readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']> | ||||
|     readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> | ||||
|     readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> | ||||
|     readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> | ||||
|  | ||||
							
								
								
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -135,6 +135,7 @@ declare module '@vue/runtime-core' { | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NH1: typeof import('naive-ui')['NH1'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|  | ||||
							
								
								
									
										14501
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14501
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -128,7 +128,7 @@ function activateOption(option: PaletteOption) { | ||||
|       <c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable /> | ||||
| 
 | ||||
|       <div v-for="(options, category) in filteredSearchResult" :key="category"> | ||||
|         <div ml-3 mt-3 text-sm font-bold text-primary op-60> | ||||
|         <div ml-3 mt-3 text-sm text-primary font-bold op-60> | ||||
|           {{ category }} | ||||
|         </div> | ||||
|         <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| import { expect, test } from '@playwright/test'; | ||||
| 
 | ||||
| test.describe('Tool - Text to ASCII binary', () => { | ||||
| test.describe('Tool - Text to UTF-8 binary', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('/text-to-binary'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Has correct title', async ({ page }) => { | ||||
|     await expect(page).toHaveTitle('Text to ASCII binary - IT Tools'); | ||||
|     await expect(page).toHaveTitle('Text to UTF-8 binary - IT Tools'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Text to binary conversion', async ({ page }) => { | ||||
| @ -17,7 +17,9 @@ test.describe('Tool - Text to ASCII binary', () => { | ||||
|   }); | ||||
| 
 | ||||
|   test('Binary to text conversion', async ({ page }) => { | ||||
|     await page.getByTestId('binary-to-text-input').fill('01101001 01110100 00101101 01110100 01101111 01101111 01101100 01110011'); | ||||
|     await page | ||||
|       .getByTestId('binary-to-text-input') | ||||
|       .fill('01101001 01110100 00101101 01110100 01101111 01101111 01101100 01110011'); | ||||
|     const text = await page.getByTestId('binary-to-text-output').inputValue(); | ||||
| 
 | ||||
|     expect(text).toEqual('it-tools'); | ||||
|  | ||||
| @ -1,32 +1,103 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { convertAsciiBinaryToText, convertTextToAsciiBinary } from './text-to-binary.models'; | ||||
| import { convertTextToUtf8Binary, convertUtf8BinaryToText } from './text-to-binary.models'; | ||||
| 
 | ||||
| describe('text-to-binary', () => { | ||||
|   describe('convertTextToAsciiBinary', () => { | ||||
|     it('a text string is converted to its ascii binary representation', () => { | ||||
|       expect(convertTextToAsciiBinary('A')).toBe('01000001'); | ||||
|       expect(convertTextToAsciiBinary('hello')).toBe('01101000 01100101 01101100 01101100 01101111'); | ||||
|       expect(convertTextToAsciiBinary('')).toBe(''); | ||||
|   const utf8Tests = [ | ||||
|     { | ||||
|       text: '文字', | ||||
|       binary: '11100110 10010110 10000111 11100101 10101101 10010111', | ||||
|       decimal: '', | ||||
|       octal: '', | ||||
|       hex: '', | ||||
|     }, | ||||
|     { | ||||
|       text: '💩', | ||||
|       binary: '11110000 10011111 10010010 10101001', | ||||
|       decimal: '', | ||||
|       octal: '', | ||||
|       hex: '', | ||||
|     }, | ||||
|   ]; | ||||
| 
 | ||||
|   describe('convertTextToUtf8Binary', () => { | ||||
|     it('a text string is converted to its UTF-8 binary representation', () => { | ||||
|       expect(convertTextToUtf8Binary('A')).toBe('01000001'); | ||||
|       expect(convertTextToUtf8Binary('A', { base: 8 })).toBe('0101'); | ||||
|       expect(convertTextToUtf8Binary('A', { base: 10 })).toBe('65'); | ||||
|       expect(convertTextToUtf8Binary('A', { base: 16 })).toBe('41'); | ||||
|       expect(convertTextToUtf8Binary('hello')).toBe('01101000 01100101 01101100 01101100 01101111'); | ||||
|       expect(convertTextToUtf8Binary('hello', { base: 8 })).toBe('0150 0145 0154 0154 0157'); | ||||
|       expect(convertTextToUtf8Binary('hello', { base: 10 })).toBe('104 101 108 108 111'); | ||||
|       expect(convertTextToUtf8Binary('hello', { base: 16 })).toBe('68 65 6c 6c 6f'); | ||||
|       expect(convertTextToUtf8Binary('')).toBe(''); | ||||
|     }); | ||||
|     it('the separator between octets can be changed', () => { | ||||
|       expect(convertTextToAsciiBinary('hello', { separator: '' })).toBe('0110100001100101011011000110110001101111'); | ||||
|       expect(convertTextToUtf8Binary('hello', { separator: '' })).toBe('0110100001100101011011000110110001101111'); | ||||
|       expect(convertTextToUtf8Binary('hello', { separator: '-' })).toBe('01101000-01100101-01101100-01101100-01101111'); | ||||
|       expect(convertTextToUtf8Binary('hello', { separator: '-', base: 16 })).toBe('68-65-6c-6c-6f'); | ||||
|     }); | ||||
|     it('works with non-ASCII input', () => { | ||||
|       for (const { text, binary } of utf8Tests) { | ||||
|         const converted = convertTextToUtf8Binary(text); | ||||
|         expect(converted).toBe(binary); | ||||
|       } | ||||
| 
 | ||||
|       expect(convertTextToUtf8Binary('💩 A', { base: 2 })).toBe('11110000 10011111 10010010 10101001 00100000 01000001'); | ||||
|       expect(convertTextToUtf8Binary('💩 A', { base: 8 })).toBe('0360 0237 0222 0251 040 0101'); | ||||
|       expect(convertTextToUtf8Binary('💩 A', { base: 10 })).toBe('240 159 146 169 32 65'); | ||||
|       expect(convertTextToUtf8Binary('💩 A', { base: 16 })).toBe('f0 9f 92 a9 20 41'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('convertAsciiBinaryToText', () => { | ||||
|   describe('convertUtf8BinaryToText', () => { | ||||
|     it('an ascii binary string is converted to its text representation', () => { | ||||
|       expect(convertAsciiBinaryToText('01101000 01100101 01101100 01101100 01101111')).toBe('hello'); | ||||
|       expect(convertAsciiBinaryToText('01000001')).toBe('A'); | ||||
|       expect(convertTextToAsciiBinary('')).toBe(''); | ||||
|       expect(convertUtf8BinaryToText('01101000 01100101 01101100 01101100 01101111')).toBe('hello'); | ||||
|       expect(convertUtf8BinaryToText('01101000 01100101 01101100 01101100 01101111', { base: 2 })).toBe('hello'); | ||||
|       expect(convertUtf8BinaryToText('0150 0145 0154 0154 0157', { base: 8 })).toBe('hello'); | ||||
|       expect(convertUtf8BinaryToText('104 101 108 108 111', { base: 10 })).toBe('hello'); | ||||
|       expect(convertUtf8BinaryToText('68 65 6c 6c 6f', { base: 16 })).toBe('hello'); | ||||
| 
 | ||||
|       expect(convertUtf8BinaryToText('11110000 10011111 10010010 10101001 00100000 01000001', { base: 2 })).toBe('💩 A'); | ||||
|       expect(convertUtf8BinaryToText('0360 0237 0222 0251 040 0101', { base: 8 })).toBe('💩 A'); | ||||
|       expect(convertUtf8BinaryToText('240 159 146 169 32 65', { base: 10 })).toBe('💩 A'); | ||||
|       expect(convertUtf8BinaryToText('f0 9f 92 a9 20 41', { base: 16 })).toBe('💩 A'); | ||||
| 
 | ||||
|       expect(convertUtf8BinaryToText('')).toBe(''); | ||||
| 
 | ||||
|       expect(convertUtf8BinaryToText('01000001')).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('0101', { base: 8 })).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('65', { base: 10 })).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('41', { base: 16 })).toBe('A'); | ||||
|     }); | ||||
| 
 | ||||
|     it('the given binary string is cleaned before conversion', () => { | ||||
|       expect(convertAsciiBinaryToText('  01000 001garbage')).toBe('A'); | ||||
|     it('the given string is cleaned before conversion', () => { | ||||
|       expect(convertUtf8BinaryToText('  01000 001garbage')).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('  65garbage', { base: 10 })).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('  41xxxx', { base: 16 })).toBe('A'); | ||||
|     }); | ||||
| 
 | ||||
|     it('throws an error if the given binary string as no complete octet', () => { | ||||
|       expect(() => convertAsciiBinaryToText('010000011')).toThrow('Invalid binary string'); | ||||
|       expect(() => convertAsciiBinaryToText('1')).toThrow('Invalid binary string'); | ||||
|     it('throws an error if the given binary string is not an integer number of complete octets', () => { | ||||
|       expect(() => convertUtf8BinaryToText('010000011')).toThrow('Invalid binary string'); | ||||
|       expect(() => convertUtf8BinaryToText('010000011 010000011')).toThrow('Invalid binary string'); | ||||
|       expect(() => convertUtf8BinaryToText('1')).toThrow('Invalid binary string'); | ||||
|     }); | ||||
| 
 | ||||
|     it('throws an error if the given binary string is not valid UTF-8', () => { | ||||
|       expect(() => convertUtf8BinaryToText('11111111')).toThrow(); | ||||
|     }); | ||||
| 
 | ||||
|     it('the given string is cleaned from prefix before conversion', () => { | ||||
|       expect(convertUtf8BinaryToText('0b01000001')).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('0x41', { base: 16 })).toBe('A'); | ||||
|       expect(convertUtf8BinaryToText('0x68 0x65 0x6c 0x6c 0x6f', { base: 16 })).toBe('hello'); | ||||
|       expect(convertUtf8BinaryToText('\\x68\\x65\\x6c\\x6c\\x6f', { base: 16 })).toBe('hello'); | ||||
|     }); | ||||
| 
 | ||||
|     it('works with non-ASCII input', () => { | ||||
|       for (const { text, binary } of utf8Tests) { | ||||
|         const reverted = convertUtf8BinaryToText(binary); | ||||
|         expect(reverted).toBe(text); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -1,22 +1,67 @@ | ||||
| export { convertTextToAsciiBinary, convertAsciiBinaryToText }; | ||||
| export { convertTextToUtf8Binary, convertUtf8BinaryToText }; | ||||
| 
 | ||||
| function convertTextToAsciiBinary(text: string, { separator = ' ' }: { separator?: string } = {}): string { | ||||
|   return text | ||||
|     .split('') | ||||
|     .map(char => char.charCodeAt(0).toString(2).padStart(8, '0')) | ||||
|     .join(separator); | ||||
| export type EncodingBase = 2 | 8 | 10 | 16; | ||||
| 
 | ||||
| function convertTextToUtf8Binary(text: string, { separator = ' ', base = 2 }: { separator?: string; base?: EncodingBase } = {}): string { | ||||
|   if (!text?.trim()) { | ||||
|     return ''; | ||||
|   } | ||||
| 
 | ||||
| function convertAsciiBinaryToText(binary: string): string { | ||||
|   const cleanBinary = binary.replace(/[^01]/g, ''); | ||||
|   return [...new TextEncoder().encode(text)].map((char) => { | ||||
|     const charInBase = char.toString(base); | ||||
|     if (base === 2) { | ||||
|       return charInBase.padStart(8, '0'); | ||||
|     } | ||||
|     if (base === 8) { | ||||
|       return `0${charInBase}`; | ||||
|     } | ||||
|     if (base === 16) { | ||||
|       return charInBase.padStart(2, '0'); | ||||
|     } | ||||
| 
 | ||||
|     return charInBase; | ||||
|   }).join(separator); | ||||
| } | ||||
| 
 | ||||
| function convertUtf8BinaryToText(binary: string, { base = 2 }: { base?: EncodingBase } = {}): string { | ||||
|   if (!binary?.trim()) { | ||||
|     return ''; | ||||
|   } | ||||
| 
 | ||||
|   let codepoints: number[] = []; | ||||
|   if (base === 2) { | ||||
|     const cleanBinary = binary.replace(/0b/g, '').replace(/[^01]/g, '').trim(); | ||||
| 
 | ||||
|     if (cleanBinary.length % 8) { | ||||
|       throw new Error('Invalid binary string'); | ||||
|     } | ||||
| 
 | ||||
|   return cleanBinary | ||||
|     .split(/(\d{8})/) | ||||
|     codepoints = cleanBinary | ||||
|       .split(/([01]{8})/) | ||||
|       .filter(Boolean) | ||||
|     .map(binary => String.fromCharCode(Number.parseInt(binary, 2))) | ||||
|     .join(''); | ||||
|       .map(binary => Number.parseInt(binary, 2)); | ||||
|   } | ||||
|   else if (base === 16) { | ||||
|     const cleanBinary = binary.replace(/0x|\\x/g, '').replace(/[^0-9A-Fa-f]/g, ''); | ||||
| 
 | ||||
|     if (cleanBinary.length % 2) { | ||||
|       throw new Error('Invalid hexadecimal string'); | ||||
|     } | ||||
| 
 | ||||
|     codepoints = cleanBinary | ||||
|       .split(/([0-9A-Fa-f]{2})/) | ||||
|       .filter(Boolean) | ||||
|       .map(binary => Number.parseInt(binary, 16)); | ||||
|   } | ||||
|   else { | ||||
|     const cleanBinary = binary.replace(/0o/g, '').replace(/[^\d\s]/g, ''); | ||||
|     codepoints = cleanBinary | ||||
|       .split(/\s/) | ||||
|       .filter(Boolean) | ||||
|       .map(binary => Number.parseInt(binary, base)); | ||||
|   } | ||||
| 
 | ||||
|   return new TextDecoder(undefined, { fatal: true }).decode( | ||||
|     Uint8Array.from(codepoints), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -1,28 +1,62 @@ | ||||
| <script setup lang="ts"> | ||||
| import { convertAsciiBinaryToText, convertTextToAsciiBinary } from './text-to-binary.models'; | ||||
| import { type EncodingBase, convertTextToUtf8Binary, convertUtf8BinaryToText } from './text-to-binary.models'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { useQueryParamOrStorage } from '@/composable/queryParams'; | ||||
| 
 | ||||
| const base = useQueryParamOrStorage({ name: 'base', storageName: 'txt-bin:base', defaultValue: '2' }); | ||||
| const inputText = ref(''); | ||||
| const binaryFromText = computed(() => convertTextToAsciiBinary(inputText.value)); | ||||
| const binaryFromText = computed(() => convertTextToUtf8Binary(inputText.value, { base: Number(base.value) as EncodingBase })); | ||||
| const { copy: copyBinary } = useCopy({ source: binaryFromText }); | ||||
| 
 | ||||
| const inputBinary = ref(''); | ||||
| const textFromBinary = computed(() => withDefaultOnError(() => convertAsciiBinaryToText(inputBinary.value), '')); | ||||
| const textFromBinary = computed(() => withDefaultOnError(() => convertUtf8BinaryToText(inputBinary.value, { base: Number(base.value) as EncodingBase }), '')); | ||||
| const inputBinaryValidationRules = [ | ||||
|   { | ||||
|     validator: (value: string) => isNotThrowing(() => convertAsciiBinaryToText(value)), | ||||
|     message: 'Binary should be a valid ASCII binary string with multiples of 8 bits', | ||||
|     validator: (value: string) => isNotThrowing(() => convertUtf8BinaryToText(value)), | ||||
|     message: 'Binary should be a valid UTF-8 binary string with multiples of 8 bits', | ||||
|   }, | ||||
| ]; | ||||
| const { copy: copyText } = useCopy({ source: textFromBinary }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <c-card title="Text to ASCII binary"> | ||||
|     <c-input-text v-model:value="inputText" multiline placeholder="e.g. 'Hello world'" label="Enter text to convert to binary" autosize autofocus raw-text test-id="text-to-binary-input" /> | ||||
|     <c-input-text v-model:value="binaryFromText" label="Binary from your text" multiline raw-text readonly mt-2 placeholder="The binary representation of your text will be here" test-id="text-to-binary-output" /> | ||||
|   <div> | ||||
|     <c-select | ||||
|       v-model:value="base" | ||||
|       label="Conversion Base:" | ||||
|       label-position="left" | ||||
|       mb-2 | ||||
|       :options="[ | ||||
|         { value: '2', label: 'Binary' }, | ||||
|         { value: '8', label: 'Octal' }, | ||||
|         { value: '10', label: 'Decimal' }, | ||||
|         { value: '16', label: 'Hexadecimal' }, | ||||
|       ]" | ||||
|     /> | ||||
| 
 | ||||
|     <c-card title="Text to UTF-8 binary"> | ||||
|       <c-input-text | ||||
|         v-model:value="inputText" | ||||
|         multiline | ||||
|         placeholder="e.g. 'Hello world'" | ||||
|         label="Enter text to convert to binary" | ||||
|         autosize | ||||
|         autofocus | ||||
|         raw-text | ||||
|         test-id="text-to-binary-input" | ||||
|       /> | ||||
|       <c-input-text | ||||
|         v-model:value="binaryFromText" | ||||
|         label="Binary from your text" | ||||
|         multiline | ||||
|         raw-text | ||||
|         readonly | ||||
|         mt-2 | ||||
|         placeholder="The binary representation of your text will be here" | ||||
|         test-id="text-to-binary-output" | ||||
|       /> | ||||
|       <div mt-2 flex justify-center> | ||||
|         <c-button :disabled="!binaryFromText" @click="copyBinary()"> | ||||
|           Copy binary to clipboard | ||||
| @ -30,13 +64,32 @@ const { copy: copyText } = useCopy({ source: textFromBinary }); | ||||
|       </div> | ||||
|     </c-card> | ||||
| 
 | ||||
|   <c-card title="ASCII binary to text"> | ||||
|     <c-input-text v-model:value="inputBinary" multiline placeholder="e.g. '01001000 01100101 01101100 01101100 01101111'" label="Enter binary to convert to text" autosize raw-text :validation-rules="inputBinaryValidationRules" test-id="binary-to-text-input" /> | ||||
|     <c-input-text v-model:value="textFromBinary" label="Text from your binary" multiline raw-text readonly mt-2 placeholder="The text representation of your binary will be here" test-id="binary-to-text-output" /> | ||||
|     <c-card title="UTF-8 binary to text"> | ||||
|       <c-input-text | ||||
|         v-model:value="inputBinary" | ||||
|         multiline | ||||
|         placeholder="e.g. '01001000 01100101 01101100 01101100 01101111'" | ||||
|         label="Enter binary to convert to text" | ||||
|         autosize | ||||
|         raw-text | ||||
|         :validation-rules="inputBinaryValidationRules" | ||||
|         test-id="binary-to-text-input" | ||||
|       /> | ||||
|       <c-input-text | ||||
|         v-model:value="textFromBinary" | ||||
|         label="Text from your binary" | ||||
|         multiline | ||||
|         raw-text | ||||
|         readonly | ||||
|         mt-2 | ||||
|         placeholder="The text representation of your binary will be here" | ||||
|         test-id="binary-to-text-output" | ||||
|       /> | ||||
|       <div mt-2 flex justify-center> | ||||
|         <c-button :disabled="!textFromBinary" @click="copyText()"> | ||||
|           Copy text to clipboard | ||||
|         </c-button> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @ -10,16 +10,18 @@ test.describe('Tool - Text to Unicode', () => { | ||||
|   }); | ||||
| 
 | ||||
|   test('Text to unicode conversion', async ({ page }) => { | ||||
|     await page.getByTestId('text-to-unicode-input').fill('it-tools'); | ||||
|     await page.getByTestId('text-to-unicode-input').fill('"it-tools" 文字'); | ||||
|     const unicode = await page.getByTestId('text-to-unicode-output').inputValue(); | ||||
| 
 | ||||
|     expect(unicode).toEqual('it-tools'); | ||||
|     // eslint-disable-next-line unicorn/escape-case
 | ||||
|     expect(unicode).toEqual(String.raw`\u0022it-tools\u0022 \u6587\u5b57`); | ||||
|   }); | ||||
| 
 | ||||
|   test('Unicode to text conversion', async ({ page }) => { | ||||
|     await page.getByTestId('unicode-to-text-input').fill('it-tools'); | ||||
|     // eslint-disable-next-line unicorn/escape-case
 | ||||
|     await page.getByTestId('unicode-to-text-input').fill(String.raw`\u0022it-tools\u0022 \u6587\u5b57`); | ||||
|     const text = await page.getByTestId('unicode-to-text-output').inputValue(); | ||||
| 
 | ||||
|     expect(text).toEqual('it-tools'); | ||||
|     expect(text).toEqual('"it-tools" 文字'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -5,7 +5,18 @@ describe('text-to-unicode', () => { | ||||
|   describe('convertTextToUnicode', () => { | ||||
|     it('a text string is converted to unicode representation', () => { | ||||
|       expect(convertTextToUnicode('A')).toBe('A'); | ||||
|       expect(convertTextToUnicode('linke the string convert to unicode')).toBe('linke the string convert to unicode'); | ||||
|       expect(convertTextToUnicode('💩 AĀ')).toBe('💩 AĀ'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'antiuni' })).toBe('\\u1f4a9\\u20\\u41\\u0100'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'css' })).toBe('\\01f4a9\\000020\\000041\\000100'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'htmldec' })).toBe('💩 AĀ'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'htmlhex' })).toBe('💩 AĀ'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'uniplus' })).toBe('U+1f4a9 U+00020 U+00041 U+00100'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'python' })).toBe('\\U1f4a9\\x20\\x41\\u0100'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'js' })).toBe('\\u{1f4a9}\\u0020\\u0041\\u0100'); | ||||
|       expect(convertTextToUnicode('💩 AĀ', { encoding: 'utf16' })).toBe('\\ud83d\\udca9\\u0020\\u0041\\u0100'); | ||||
|       expect(convertTextToUnicode('💩 hello AĀ', { skipAscii: true })).toBe('💩 hello AĀ'); | ||||
|       expect(convertTextToUnicode('linke the string convert to unicode')).toBe( | ||||
|         'linke the string convert to unicode'); | ||||
|       expect(convertTextToUnicode('')).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
| @ -13,6 +24,16 @@ describe('text-to-unicode', () => { | ||||
|   describe('convertUnicodeToText', () => { | ||||
|     it('an unicode string is converted to its text representation', () => { | ||||
|       expect(convertUnicodeToText('A')).toBe('A'); | ||||
|       expect(convertUnicodeToText('\\u1f4a9\\u20\\u41\\u0100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('\\01f4a9\\000020\\000041\\000100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('💩 AĀ')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('💩 AĀ')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('U+1f4a9 U+00020 U+00041 U+00100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('\\U1f4a9\\x20\\x41\\u0100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('\\u{1f4a9}\\u0020\\u0041\\u0100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('\\ud83d\\udca9\\u0020\\u0041\\u0100')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('\\01f4a9 AĀ')).toBe('💩 AĀ'); | ||||
|       expect(convertUnicodeToText('💩 hello AĀ')).toBe('💩 hello AĀ'); | ||||
|       expect(convertUnicodeToText('linke the string convert to unicode')).toBe('linke the string convert to unicode'); | ||||
|       expect(convertUnicodeToText('')).toBe(''); | ||||
|     }); | ||||
|  | ||||
| @ -1,9 +1,66 @@ | ||||
| function convertTextToUnicode(text: string): string { | ||||
|   return text.split('').map(value => `&#${value.charCodeAt(0)};`).join(''); | ||||
| export type Encoding = 'htmldec' | 'htmlhex' | 'uniplus' | 'antiuni' | 'css' | 'python' | 'js' | 'utf16'; | ||||
| 
 | ||||
| const ALL_PRINTABLE_ASCII = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; | ||||
| 
 | ||||
| function convertTextToUnicode(text: string, { encoding = 'htmldec', skipAscii = false }: { encoding?: Encoding; skipAscii?: boolean } = {}): string { | ||||
|   let prefix: (value: number) => string; | ||||
|   let suffix: (value: number) => string = () => ''; | ||||
|   let base = 16; | ||||
|   let padding: (value: number) => number = () => 0; | ||||
|   let separator = ''; | ||||
|   let codepoints = [...text]; | ||||
|   if (encoding === 'htmldec') { | ||||
|     prefix = () => '&#'; | ||||
|     base = 10; | ||||
|     suffix = () => ';'; | ||||
|   } | ||||
|   else if (encoding === 'htmlhex') { | ||||
|     prefix = () => '&#x'; | ||||
|     suffix = () => ';'; | ||||
|   } | ||||
|   else if (encoding === 'uniplus') { | ||||
|     prefix = () => 'U+'; | ||||
|     padding = () => 5; | ||||
|     separator = ' '; | ||||
|   } | ||||
|   else if (encoding === 'antiuni') { | ||||
|     prefix = () => '\\u'; | ||||
|     padding = (value: number) => value < 256 ? 2 : 4; | ||||
|   } | ||||
|   else if (encoding === 'utf16') { | ||||
|     prefix = () => '\\u'; | ||||
|     padding = () => 4; | ||||
|     codepoints = text.split(''); | ||||
|   } | ||||
|   else if (encoding === 'python') { | ||||
|     prefix = (value: number) => value < 256 ? '\\x' : (value < 65536 ? '\\u' : '\\U'); | ||||
|     padding = (value: number) => value < 256 ? 2 : 4; | ||||
|   } | ||||
|   else if (encoding === 'js') { | ||||
|     prefix = (value: number) => value < 65536 ? '\\u' : '\\u{'; | ||||
|     suffix = (value: number) => value < 65536 ? '' : '}'; | ||||
|     padding = () => 4; | ||||
|   } | ||||
|   else if (encoding === 'css') { | ||||
|     prefix = () => '\\'; | ||||
|     padding = () => 6; | ||||
|   } | ||||
| 
 | ||||
|   return codepoints.map((value) => { | ||||
|     if (skipAscii && ALL_PRINTABLE_ASCII.includes(value)) { | ||||
|       return value; | ||||
|     } | ||||
|     const charCode = value.codePointAt(0) || 0xFF; | ||||
|     return `${prefix(charCode)}${charCode.toString(base).padStart(padding(charCode), '0')}${suffix(charCode)}`; | ||||
|   }).join(separator); | ||||
| } | ||||
| 
 | ||||
| function convertUnicodeToText(unicodeStr: string): string { | ||||
|   return unicodeStr.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec)); | ||||
|   return unicodeStr | ||||
|     .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(dec)) | ||||
|     .replace(/&#[xX]([0-9A-Fa-f]+);/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))) | ||||
|     .replace(/\\u\{([0-9A-Fa-f]+)\}/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))) | ||||
|     .replace(/(?:\\[uUx]|\\|\s*U\+)([0-9A-Fa-f]+)/g, (match, hex) => String.fromCodePoint(Number.parseInt(hex, 16))); // NOSONAR
 | ||||
| } | ||||
| 
 | ||||
| export { convertTextToUnicode, convertUnicodeToText }; | ||||
|  | ||||
| @ -1,18 +1,46 @@ | ||||
| <script setup lang="ts"> | ||||
| import { convertTextToUnicode, convertUnicodeToText } from './text-to-unicode.service'; | ||||
| import { type Encoding, convertTextToUnicode, convertUnicodeToText } from './text-to-unicode.service'; | ||||
| import { useQueryParamOrStorage } from '@/composable/queryParams'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const encoding = useQueryParamOrStorage({ name: 'enc', storageName: 'txt-uni:enc', defaultValue: 'htmldec' }); | ||||
| const skipAscii = useQueryParamOrStorage({ name: 'skipAscii', storageName: 'txt-uni:asc', defaultValue: false }); | ||||
| 
 | ||||
| const inputText = ref(''); | ||||
| const unicodeFromText = computed(() => inputText.value.trim() === '' ? '' : convertTextToUnicode(inputText.value)); | ||||
| const unicodeFromText = computed(() => inputText.value.trim() === '' | ||||
|   ? '' | ||||
|   : convertTextToUnicode(inputText.value, { encoding: encoding.value as Encoding, skipAscii: skipAscii.value })); | ||||
| const { copy: copyUnicode } = useCopy({ source: unicodeFromText }); | ||||
| 
 | ||||
| const inputUnicode = ref(''); | ||||
| const textFromUnicode = computed(() => inputUnicode.value.trim() === '' ? '' : convertUnicodeToText(inputUnicode.value)); | ||||
| const textFromUnicode = computed(() => inputUnicode.value.trim() === '' | ||||
|   ? '' | ||||
|   : convertUnicodeToText(inputUnicode.value)); | ||||
| const { copy: copyText } = useCopy({ source: textFromUnicode }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <c-card title="Text to Unicode"> | ||||
|     <c-select | ||||
|       v-model:value="encoding" | ||||
|       label="Unicode encoding" | ||||
|       :options="[ | ||||
|         { value: 'htmldec', label: 'HTML Decimal (&#160;)' }, | ||||
|         { value: 'htmlhex', label: 'HTML Hexadecimal (&#xA0;)' }, | ||||
|         { value: 'uniplus', label: 'U+00A0' }, | ||||
|         { value: 'antiuni', label: '\\u00A0' }, | ||||
|         { value: 'css', label: 'CSS (\\0000A0)' }, | ||||
|         { value: 'python', label: 'Python (\\xA0, \\u00A0, \\U100A0)' }, | ||||
|         { value: 'js', label: 'Python (\\u00A0; \\u{100A0})' }, | ||||
|         { value: 'utf16', label: 'UTF 16 (with surrogates)' }, | ||||
|       ]" | ||||
|     /> | ||||
|     <n-form-item> | ||||
|       <n-checkbox v-model:checked="skipAscii"> | ||||
|         Skip Ascii characters | ||||
|       </n-checkbox> | ||||
|     </n-form-item> | ||||
| 
 | ||||
|     <c-input-text v-model:value="inputText" multiline placeholder="e.g. 'Hello Avengers'" label="Enter text to convert to unicode" autosize autofocus raw-text test-id="text-to-unicode-input" /> | ||||
|     <c-input-text v-model:value="unicodeFromText" label="Unicode from your text" multiline raw-text readonly mt-2 placeholder="The unicode representation of your text will be here" test-id="text-to-unicode-output" /> | ||||
|     <div mt-2 flex justify-center> | ||||
|  | ||||
| @ -151,7 +151,7 @@ function onSearchInput() { | ||||
|       > | ||||
|         <div flex-1 truncate> | ||||
|           <slot name="displayed-value"> | ||||
|             <input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput"> | ||||
|             <input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full color-current lh-normal @input="onSearchInput"> | ||||
|             <span v-else-if="selectedOption" lh-normal> | ||||
|               {{ selectedOption.label }} | ||||
|             </span> | ||||
|  | ||||
| @ -39,7 +39,7 @@ const headers = computed(() => { | ||||
| <template> | ||||
|   <div class="relative overflow-x-auto rounded"> | ||||
|     <table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description"> | ||||
|       <thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5"> | ||||
|       <thead v-if="!hideHeaders" class="bg-#ffffff text-gray-700 uppercase dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5"> | ||||
|         <tr> | ||||
|           <th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs"> | ||||
|             {{ header.label }} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user