This commit is contained in:
sharevb 2025-04-06 18:42:01 -07:00 committed by GitHub
commit 559e6dc423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 6936 additions and 8553 deletions

View file

@ -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
View file

@ -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
View file

@ -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']

15049
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -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" />

View file

@ -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');

View file

@ -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);
}
});
});
});

View file

@ -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 convertAsciiBinaryToText(binary: string): string {
const cleanBinary = binary.replace(/[^01]/g, '');
if (cleanBinary.length % 8) {
throw new Error('Invalid binary string');
function convertTextToUtf8Binary(text: string, { separator = ' ', base = 2 }: { separator?: string; base?: EncodingBase } = {}): string {
if (!text?.trim()) {
return '';
}
return cleanBinary
.split(/(\d{8})/)
.filter(Boolean)
.map(binary => String.fromCharCode(Number.parseInt(binary, 2)))
.join('');
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');
}
codepoints = cleanBinary
.split(/([01]{8})/)
.filter(Boolean)
.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),
);
}

View file

@ -1,42 +1,95 @@
<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 mt-2 flex justify-center>
<c-button :disabled="!binaryFromText" @click="copyBinary()">
Copy binary to clipboard
</c-button>
</div>
</c-card>
<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="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" />
<div mt-2 flex justify-center>
<c-button :disabled="!textFromBinary" @click="copyText()">
Copy text to clipboard
</c-button>
</div>
</c-card>
<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
</c-button>
</div>
</c-card>
<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>

View file

@ -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('&#105;&#116;&#45;&#116;&#111;&#111;&#108;&#115;');
// 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('&#105;&#116;&#45;&#116;&#111;&#111;&#108;&#115;');
// 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" 文字');
});
});

View file

@ -5,7 +5,18 @@ describe('text-to-unicode', () => {
describe('convertTextToUnicode', () => {
it('a text string is converted to unicode representation', () => {
expect(convertTextToUnicode('A')).toBe('&#65;');
expect(convertTextToUnicode('linke the string convert to unicode')).toBe('&#108;&#105;&#110;&#107;&#101;&#32;&#116;&#104;&#101;&#32;&#115;&#116;&#114;&#105;&#110;&#103;&#32;&#99;&#111;&#110;&#118;&#101;&#114;&#116;&#32;&#116;&#111;&#32;&#117;&#110;&#105;&#99;&#111;&#100;&#101;');
expect(convertTextToUnicode('💩 AĀ')).toBe('&#128169;&#32;&#65;&#256;');
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('&#128169;&#32;&#65;&#256;');
expect(convertTextToUnicode('💩 AĀ', { encoding: 'htmlhex' })).toBe('&#x1f4a9;&#x20;&#x41;&#x100;');
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('&#128169; hello A&#256;');
expect(convertTextToUnicode('linke the string convert to unicode')).toBe(
'&#108;&#105;&#110;&#107;&#101;&#32;&#116;&#104;&#101;&#32;&#115;&#116;&#114;&#105;&#110;&#103;&#32;&#99;&#111;&#110;&#118;&#101;&#114;&#116;&#32;&#116;&#111;&#32;&#117;&#110;&#105;&#99;&#111;&#100;&#101;');
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('&#65;')).toBe('A');
expect(convertUnicodeToText('\\u1f4a9\\u20\\u41\\u0100')).toBe('💩 AĀ');
expect(convertUnicodeToText('\\01f4a9\\000020\\000041\\000100')).toBe('💩 AĀ');
expect(convertUnicodeToText('&#128169;&#32;&#65;&#256;')).toBe('💩 AĀ');
expect(convertUnicodeToText('&#x1f4a9;&#x20;&#x41;&#x100;')).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('&#128169; hello A&#256;')).toBe('💩 hello AĀ');
expect(convertUnicodeToText('&#108;&#105;&#110;&#107;&#101;&#32;&#116;&#104;&#101;&#32;&#115;&#116;&#114;&#105;&#110;&#103;&#32;&#99;&#111;&#110;&#118;&#101;&#114;&#116;&#32;&#116;&#111;&#32;&#117;&#110;&#105;&#99;&#111;&#100;&#101;')).toBe('linke the string convert to unicode');
expect(convertUnicodeToText('')).toBe('');
});

View file

@ -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 };

View file

@ -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 (&amp;#160;)' },
{ value: 'htmlhex', label: 'HTML Hexadecimal (&amp;#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>

View file

@ -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>

View file

@ -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 }}