fix(text-to-binary): return valid UTF-8 results for non-ASCII text
This commit is contained in:
		
							parent
							
								
									e876d03608
								
							
						
					
					
						commit
						35d8264c2b
					
				| @ -389,5 +389,5 @@ tools: | |||||||
|     description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it. |     description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it. | ||||||
| 
 | 
 | ||||||
|   text-to-binary: |   text-to-binary: | ||||||
|     title: Text to ASCII binary |     title: Text to UTF-8 binary | ||||||
|     description: Convert text to its ASCII binary representation and vice-versa. |     description: Convert text to its UTF-8 binary representation and vice-versa. | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| import { expect, test } from '@playwright/test'; | 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 }) => { |   test.beforeEach(async ({ page }) => { | ||||||
|     await page.goto('/text-to-binary'); |     await page.goto('/text-to-binary'); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test('Has correct title', async ({ page }) => { |   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 }) => { |   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 }) => { |   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(); |     const text = await page.getByTestId('binary-to-text-output').inputValue(); | ||||||
| 
 | 
 | ||||||
|     expect(text).toEqual('it-tools'); |     expect(text).toEqual('it-tools'); | ||||||
|  | |||||||
| @ -1,32 +1,56 @@ | |||||||
| import { describe, expect, it } from 'vitest'; | 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('text-to-binary', () => { | ||||||
|   describe('convertTextToAsciiBinary', () => { |   const utf8Tests = [ | ||||||
|     it('a text string is converted to its ascii binary representation', () => { |     { text: '文字', binary: '11100110 10010110 10000111 11100101 10101101 10010111' }, | ||||||
|       expect(convertTextToAsciiBinary('A')).toBe('01000001'); |     { text: '💩', binary: '11110000 10011111 10010010 10101001' }, | ||||||
|       expect(convertTextToAsciiBinary('hello')).toBe('01101000 01100101 01101100 01101100 01101111'); |   ]; | ||||||
|       expect(convertTextToAsciiBinary('')).toBe(''); | 
 | ||||||
|  |   describe('convertTextToUtf8Binary', () => { | ||||||
|  |     it('a text string is converted to its UTF-8 binary representation', () => { | ||||||
|  |       expect(convertTextToUtf8Binary('A')).toBe('01000001'); | ||||||
|  |       expect(convertTextToUtf8Binary('hello')).toBe('01101000 01100101 01101100 01101100 01101111'); | ||||||
|  |       expect(convertTextToUtf8Binary('')).toBe(''); | ||||||
|     }); |     }); | ||||||
|     it('the separator between octets can be changed', () => { |     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'); | ||||||
|  |     }); | ||||||
|  |     it('works with non-ASCII input', () => { | ||||||
|  |       for (const { text, binary } of utf8Tests) { | ||||||
|  |         const converted = convertTextToUtf8Binary(text); | ||||||
|  |         expect(converted).toBe(binary); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('convertAsciiBinaryToText', () => { |   describe('convertUtf8BinaryToText', () => { | ||||||
|     it('an ascii binary string is converted to its text representation', () => { |     it('an ascii binary string is converted to its text representation', () => { | ||||||
|       expect(convertAsciiBinaryToText('01101000 01100101 01101100 01101100 01101111')).toBe('hello'); |       expect(convertUtf8BinaryToText('01101000 01100101 01101100 01101100 01101111')).toBe('hello'); | ||||||
|       expect(convertAsciiBinaryToText('01000001')).toBe('A'); |       expect(convertUtf8BinaryToText('01000001')).toBe('A'); | ||||||
|       expect(convertTextToAsciiBinary('')).toBe(''); |       expect(convertTextToUtf8Binary('')).toBe(''); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('the given binary string is cleaned before conversion', () => { |     it('the given binary string is cleaned before conversion', () => { | ||||||
|       expect(convertAsciiBinaryToText('  01000 001garbage')).toBe('A'); |       expect(convertUtf8BinaryToText('  01000 001garbage')).toBe('A'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('throws an error if the given binary string as no complete octet', () => { |     it('throws an error if the given binary string is not an integer number of complete octets', () => { | ||||||
|       expect(() => convertAsciiBinaryToText('010000011')).toThrow('Invalid binary string'); |       expect(() => convertUtf8BinaryToText('010000011')).toThrow('Invalid binary string'); | ||||||
|       expect(() => convertAsciiBinaryToText('1')).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('works with non-ASCII input', () => { | ||||||
|  |       for (const { text, binary } of utf8Tests) { | ||||||
|  |         const reverted = convertUtf8BinaryToText(binary); | ||||||
|  |         expect(reverted).toBe(text); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,22 +1,19 @@ | |||||||
| export { convertTextToAsciiBinary, convertAsciiBinaryToText }; | export { convertTextToUtf8Binary, convertUtf8BinaryToText }; | ||||||
| 
 | 
 | ||||||
| function convertTextToAsciiBinary(text: string, { separator = ' ' }: { separator?: string } = {}): string { | function convertTextToUtf8Binary(text: string, { separator = ' ' }: { separator?: string } = {}): string { | ||||||
|   return text |   return [...new TextEncoder().encode(text)].map(x => x.toString(2).padStart(8, '0')).join(separator); | ||||||
|     .split('') |  | ||||||
|     .map(char => char.charCodeAt(0).toString(2).padStart(8, '0')) |  | ||||||
|     .join(separator); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function convertAsciiBinaryToText(binary: string): string { | function convertUtf8BinaryToText(binary: string): string { | ||||||
|   const cleanBinary = binary.replace(/[^01]/g, ''); |   const cleanBinary = binary.replace(/[^01]+/g, ''); | ||||||
| 
 | 
 | ||||||
|   if (cleanBinary.length % 8) { |   if (cleanBinary.length % 8) { | ||||||
|     throw new Error('Invalid binary string'); |     throw new Error('Invalid binary string'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return cleanBinary |   return new TextDecoder(undefined, { fatal: true }).decode( | ||||||
|     .split(/(\d{8})/) |     Uint8Array.from({ length: cleanBinary.length / 8 }, (_, i) => | ||||||
|     .filter(Boolean) |       Number.parseInt(cleanBinary.slice(i * 8, (i + 1) * 8), 2), | ||||||
|     .map(binary => String.fromCharCode(Number.parseInt(binary, 2))) |     ), | ||||||
|     .join(''); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,42 +1,74 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { convertAsciiBinaryToText, convertTextToAsciiBinary } from './text-to-binary.models'; | import { convertTextToUtf8Binary, convertUtf8BinaryToText } from './text-to-binary.models'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import { useCopy } from '@/composable/copy'; | import { useCopy } from '@/composable/copy'; | ||||||
| import { isNotThrowing } from '@/utils/boolean'; | import { isNotThrowing } from '@/utils/boolean'; | ||||||
| 
 | 
 | ||||||
| const inputText = ref(''); | const inputText = ref(''); | ||||||
| const binaryFromText = computed(() => convertTextToAsciiBinary(inputText.value)); | const binaryFromText = computed(() => convertTextToUtf8Binary(inputText.value)); | ||||||
| const { copy: copyBinary } = useCopy({ source: binaryFromText }); | const { copy: copyBinary } = useCopy({ source: binaryFromText }); | ||||||
| 
 | 
 | ||||||
| const inputBinary = ref(''); | const inputBinary = ref(''); | ||||||
| const textFromBinary = computed(() => withDefaultOnError(() => convertAsciiBinaryToText(inputBinary.value), '')); | const textFromBinary = computed(() => withDefaultOnError(() => convertUtf8BinaryToText(inputBinary.value), '')); | ||||||
| const inputBinaryValidationRules = [ | const inputBinaryValidationRules = [ | ||||||
|   { |   { | ||||||
|     validator: (value: string) => isNotThrowing(() => convertAsciiBinaryToText(value)), |     validator: (value: string) => isNotThrowing(() => convertUtf8BinaryToText(value)), | ||||||
|     message: 'Binary should be a valid ASCII binary string with multiples of 8 bits', |     message: 'Binary should be a valid UTF-8 binary string with multiples of 8 bits', | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| const { copy: copyText } = useCopy({ source: textFromBinary }); | const { copy: copyText } = useCopy({ source: textFromBinary }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="Text to ASCII binary"> |   <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 | ||||||
|     <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" /> |       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> |     <div mt-2 flex justify-center> | ||||||
|       <c-button :disabled="!binaryFromText" @click="copyBinary()"> |       <c-button :disabled="!binaryFromText" @click="copyBinary()"> Copy binary to clipboard </c-button> | ||||||
|         Copy binary to clipboard |  | ||||||
|       </c-button> |  | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| 
 | 
 | ||||||
|   <c-card title="ASCII binary to text"> |   <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 | ||||||
|     <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" /> |       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> |     <div mt-2 flex justify-center> | ||||||
|       <c-button :disabled="!textFromBinary" @click="copyText()"> |       <c-button :disabled="!textFromBinary" @click="copyText()"> Copy text to clipboard </c-button> | ||||||
|         Copy text to clipboard |  | ||||||
|       </c-button> |  | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user