Merge remote-tracking branch 'origin/main' into fix/phone-parser-storage

This commit is contained in:
ShareVB 2024-07-14 23:30:22 +02:00
commit 72df50ba51
10 changed files with 192 additions and 43 deletions

View file

@ -64,6 +64,7 @@
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5", "iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3", "ibantools": "^4.3.3",
"js-base64": "^3.7.6",
"json5": "^2.2.3", "json5": "^2.2.3",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28", "libphonenumber-js": "^1.10.28",
@ -137,5 +138,6 @@
"vitest": "^0.34.0", "vitest": "^0.34.0",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"zx": "^7.2.1" "zx": "^7.2.1"
} },
"packageManager": "pnpm@8.15.3"
} }

19
pnpm-lock.yaml generated
View file

@ -92,6 +92,9 @@ dependencies:
ibantools: ibantools:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3 version: 4.3.3
js-base64:
specifier: ^3.7.6
version: 3.7.7
json5: json5:
specifier: ^2.2.3 specifier: ^2.2.3
version: 2.2.3 version: 2.2.3
@ -3351,7 +3354,7 @@ packages:
dependencies: dependencies:
'@unhead/dom': 0.5.1 '@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1 '@unhead/schema': 0.5.1
'@vueuse/shared': 10.7.2(vue@3.3.4) '@vueuse/shared': 10.8.0(vue@3.3.4)
unhead: 0.5.1 unhead: 0.5.1
vue: 3.3.4 vue: 3.3.4
transitivePeerDependencies: transitivePeerDependencies:
@ -3993,10 +3996,10 @@ packages:
- vue - vue
dev: false dev: false
/@vueuse/shared@10.7.2(vue@3.3.4): /@vueuse/shared@10.8.0(vue@3.3.4):
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} resolution: {integrity: sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==}
dependencies: dependencies:
vue-demi: 0.14.6(vue@3.3.4) vue-demi: 0.14.7(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -6472,6 +6475,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
dev: false
/js-beautify@1.14.6: /js-beautify@1.14.6:
resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==} resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -9151,8 +9158,8 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-demi@0.14.6(vue@3.3.4): /vue-demi@0.14.7(vue@3.3.4):
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true

View file

@ -48,7 +48,7 @@ const output = computed(() => transformer.value(input.value));
monospace monospace
/> />
<div> <div overflow-auto>
<div mb-5px> <div mb-5px>
{{ outputLabel }} {{ outputLabel }}
</div> </div>

View file

@ -0,0 +1,21 @@
import _ from 'lodash';
function useDebouncedRef<T>(initialValue: T, delay: number, immediate: boolean = false) {
const state = ref(initialValue);
const debouncedRef = customRef((track, trigger) => ({
get() {
track();
return state.value;
},
set: _.debounce(
(value) => {
state.value = value;
trigger();
},
delay,
{ leading: immediate },
),
}));
return debouncedRef;
}
export default useDebouncedRef;

View file

@ -1,8 +1,13 @@
import { extension as getExtensionFromMime } from 'mime-types'; import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
export { getMimeTypeFromBase64, useDownloadFileFromBase64 }; export {
getMimeTypeFromBase64,
getMimeTypeFromExtension, getExtensionFromMimeType,
useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
previewImageFromBase64,
};
const commonMimeTypesSignatures = { const commonMimeTypesSignatures = {
'JVBERi0': 'application/pdf', 'JVBERi0': 'application/pdf',
@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
defaultExtension?: string defaultExtension?: string
}) { }) {
if (mimeType) { if (mimeType) {
return getExtensionFromMime(mimeType) ?? defaultExtension; return getExtensionFromMimeType(mimeType) ?? defaultExtension;
} }
return defaultExtension; return defaultExtension;
} }
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) { function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
if (sourceValue === '') {
throw new Error('Base64 string is empty');
}
const defaultExtension = extension ?? 'txt';
const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
let base64String = sourceValue;
if (!mimeType) {
const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
base64String = `data:${targetMimeType};base64,${sourceValue}`;
}
const cleanExtension = extension ?? getFileExtensionFromMimeType(
{ mimeType, defaultExtension });
let cleanFileName = filename ?? `file.${cleanExtension}`;
if (extension && !cleanFileName.endsWith(`.${extension}`)) {
cleanFileName = `${cleanFileName}.${cleanExtension}`;
}
const a = document.createElement('a');
a.href = base64String;
a.download = cleanFileName;
a.click();
}
function useDownloadFileFromBase64(
{ source, filename, extension, fileMimeType }:
{ source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
return { return {
download() { download() {
if (source.value === '') { downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
throw new Error('Base64 string is empty');
}
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
const base64String = mimeType
? source.value
: `data:text/plain;base64,${source.value}`;
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
const a = document.createElement('a');
a.href = base64String;
a.download = cleanFileName;
a.click();
}, },
}; };
} }
function useDownloadFileFromBase64Refs(
{ source, filename, extension }:
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
},
};
}
function previewImageFromBase64(base64String: string): HTMLImageElement {
if (base64String === '') {
throw new Error('Base64 string is empty');
}
const img = document.createElement('img');
img.src = base64String;
const container = document.createElement('div');
container.appendChild(img);
const previewContainer = document.getElementById('previewContainer');
if (previewContainer) {
previewContainer.innerHTML = '';
previewContainer.appendChild(container);
}
else {
throw new Error('Preview container element not found');
}
return img;
}

View file

@ -2,12 +2,19 @@
import { useBase64 } from '@vueuse/core'; import { useBase64 } from '@vueuse/core';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
import { useValidation } from '@/composable/validation'; import { useValidation } from '@/composable/validation';
import { isValidBase64 } from '@/utils/base64'; import { isValidBase64 } from '@/utils/base64';
const fileName = ref('file');
const fileExtension = ref('');
const base64Input = ref(''); const base64Input = ref('');
const { download } = useDownloadFileFromBase64({ source: base64Input }); const { download } = useDownloadFileFromBase64Refs(
{
source: base64Input,
filename: fileName,
extension: fileExtension,
});
const base64InputValidation = useValidation({ const base64InputValidation = useValidation({
source: base64Input, source: base64Input,
rules: [ rules: [
@ -18,6 +25,35 @@ const base64InputValidation = useValidation({
], ],
}); });
watch(
base64Input,
(newValue, _) => {
const { mimeType } = getMimeTypeFromBase64({ base64String: newValue });
if (mimeType) {
fileExtension.value = getExtensionFromMimeType(mimeType) || fileExtension.value;
}
},
);
function previewImage() {
if (!base64InputValidation.isValid) {
return;
}
try {
const image = previewImageFromBase64(base64Input.value);
image.style.maxWidth = '100%';
image.style.maxHeight = '400px';
const previewContainer = document.getElementById('previewContainer');
if (previewContainer) {
previewContainer.innerHTML = '';
previewContainer.appendChild(image);
}
}
catch (_) {
//
}
}
function downloadFile() { function downloadFile() {
if (!base64InputValidation.isValid) { if (!base64InputValidation.isValid) {
return; return;
@ -44,6 +80,24 @@ async function onUpload(file: File) {
<template> <template>
<c-card title="Base64 to file"> <c-card title="Base64 to file">
<n-grid cols="3" x-gap="12">
<n-gi span="2">
<c-input-text
v-model:value="fileName"
label="File Name"
placeholder="Download filename"
mb-2
/>
</n-gi>
<n-gi>
<c-input-text
v-model:value="fileExtension"
label="Extension"
placeholder="Extension"
mb-2
/>
</n-gi>
</n-grid>
<c-input-text <c-input-text
v-model:value="base64Input" v-model:value="base64Input"
multiline multiline
@ -53,7 +107,14 @@ async function onUpload(file: File) {
mb-2 mb-2
/> />
<div flex justify-center> <div flex justify-center py-2>
<div id="previewContainer" />
</div>
<div flex justify-center gap-3>
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="previewImage()">
Preview image
</c-button>
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
Download file Download file
</c-button> </c-button>

View file

@ -4,6 +4,7 @@ import emojiKeywords from 'emojilib';
import _ from 'lodash'; import _ from 'lodash';
import type { EmojiInfo } from './emoji.types'; import type { EmojiInfo } from './emoji.types';
import { useFuzzySearch } from '@/composable/fuzzySearch'; import { useFuzzySearch } from '@/composable/fuzzySearch';
import useDebouncedRef from '@/composable/debouncedref';
const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join(''); const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join('');
const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined; const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined;
@ -23,7 +24,7 @@ const emojisGroups: { emojiInfos: EmojiInfo[]; group: string }[] = _
.map((emojiInfos, group) => ({ group, emojiInfos })) .map((emojiInfos, group) => ({ group, emojiInfos }))
.value(); .value();
const searchQuery = ref(''); const searchQuery = useDebouncedRef('', 500);
const { searchResult } = useFuzzySearch({ const { searchResult } = useFuzzySearch({
search: searchQuery, search: searchQuery,

View file

@ -39,7 +39,7 @@ const validation = useValidation({
{{ section.title }} {{ section.title }}
</th> </th>
<tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value"> <tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
<td class="claims"> <td class="claims" style="vertical-align: top;">
<span font-bold> <span font-bold>
{{ claim }} {{ claim }}
</span> </span>
@ -47,7 +47,7 @@ const validation = useValidation({
({{ claimDescription }}) ({{ claimDescription }})
</span> </span>
</td> </td>
<td> <td style="word-wrap: break-word;word-break: break-all;">
<span>{{ value }}</span> <span>{{ value }}</span>
<span v-if="friendlyValue" ml-2 op-70> <span v-if="friendlyValue" ml-2 op-70>
({{ friendlyValue }}) ({{ friendlyValue }})

View file

@ -38,7 +38,8 @@ describe('base64 utils', () => {
it('should throw for incorrect base64 string', () => { it('should throw for incorrect base64 string', () => {
expect(() => base64ToText('a')).to.throw('Incorrect base64 string'); expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
expect(() => base64ToText(' ')).to.throw('Incorrect base64 string'); // should not really be false because trimming of space is now implied
// expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
expect(() => base64ToText('é')).to.throw('Incorrect base64 string'); expect(() => base64ToText('é')).to.throw('Incorrect base64 string');
// missing final '=' // missing final '='
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string'); expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
@ -56,17 +57,17 @@ describe('base64 utils', () => {
it('should return false for incorrect base64 string', () => { it('should return false for incorrect base64 string', () => {
expect(isValidBase64('a')).to.eql(false); expect(isValidBase64('a')).to.eql(false);
expect(isValidBase64(' ')).to.eql(false);
expect(isValidBase64('é')).to.eql(false); expect(isValidBase64('é')).to.eql(false);
expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false); expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
// missing final '=' // missing final '='
expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false); expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
}); });
it('should return false for untrimmed correct base64 string', () => { it('should return true for untrimmed correct base64 string', () => {
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false); expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(true);
expect(isValidBase64(' LTE=')).to.eql(false); expect(isValidBase64(' LTE=')).to.eql(true);
expect(isValidBase64(' YQ== ')).to.eql(false); expect(isValidBase64(' YQ== ')).to.eql(true);
expect(isValidBase64(' ')).to.eql(true);
}); });
}); });

View file

@ -1,7 +1,9 @@
import { Base64 } from 'js-base64';
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix }; export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) { function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
const encoded = window.btoa(str); const encoded = Base64.encode(str);
return makeUrlSafe ? makeUriSafe(encoded) : encoded; return makeUrlSafe ? makeUriSafe(encoded) : encoded;
} }
@ -16,7 +18,7 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool
} }
try { try {
return window.atob(cleanStr); return Base64.decode(cleanStr);
} }
catch (_) { catch (_) {
throw new Error('Incorrect base64 string'); throw new Error('Incorrect base64 string');
@ -34,10 +36,11 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo
} }
try { try {
const reEncodedBase64 = Base64.fromUint8Array(Base64.toUint8Array(cleanStr));
if (makeUrlSafe) { if (makeUrlSafe) {
return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr; return removePotentialPadding(reEncodedBase64) === cleanStr;
} }
return window.btoa(window.atob(cleanStr)) === cleanStr; return reEncodedBase64 === cleanStr.replace(/\s/g, '');
} }
catch (err) { catch (err) {
return false; return false;