Compare commits

..

5 Commits

Author SHA1 Message Date
Corentin Thomasset
04a8e122be chore(release): 2.10.1 2022-08-04 12:16:54 +02:00
Corentin Thomasset
447bdf2148 refactor(base64): mutualized base64 functions into global utilities 2022-08-04 12:09:32 +02:00
Corentin Thomasset
ca7cb44389 fix(bip39-generator): cleared an issue with the mnemonic validation 2022-08-04 12:08:23 +02:00
Corentin Thomasset
e48d60b1ed refactor(chronometer): improved chronometer precision 2022-08-04 09:06:42 +02:00
Corentin Thomasset
fda0b0ca25 fix(import): removed auto added weird .js extension 2022-08-04 08:50:15 +02:00
16 changed files with 202 additions and 42 deletions

View File

@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.10.1](https://github.com/CorentinTh/it-tools/compare/v2.10.0...v2.10.1) (2022-08-04)
### Bug Fixes
* **bip39-generator:** cleared an issue with the mnemonic validation ([ca7cb44](https://github.com/CorentinTh/it-tools/commit/ca7cb4438972ca09f28a6a40332ec94ceaa4aab4))
* **import:** removed auto added weird .js extension ([fda0b0c](https://github.com/CorentinTh/it-tools/commit/fda0b0ca25c1733542a4e797ac1a2150c546a660))
### Refactors
* **base64:** mutualized base64 functions into global utilities ([447bdf2](https://github.com/CorentinTh/it-tools/commit/447bdf2148098d70ba309e13d9b1e846b5064da1))
* **chronometer:** improved chronometer precision ([e48d60b](https://github.com/CorentinTh/it-tools/commit/e48d60b1ed19279f48441743f7ed69e8fd915011))
## [2.10.0](https://github.com/CorentinTh/it-tools/compare/v2.9.2...v2.10.0) (2022-08-03)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "it-tools",
"version": "2.10.0",
"version": "2.10.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "it-tools",
"version": "2.10.0",
"version": "2.10.1",
"license": "GNU GPLv3",
"dependencies": {
"@it-tools/bip39": "^0.0.4",

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2.10.0",
"version": "2.10.1",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",

View File

@@ -35,6 +35,7 @@
import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { useValidation } from '@/composable/validation';
import { isValidBase64 } from '@/utils/base64';
import { Upload } from '@vicons/tabler';
import { useBase64 } from '@vueuse/core';
import type { UploadFileInfo } from 'naive-ui';
@@ -47,7 +48,7 @@ const base64InputValidation = useValidation({
rules: [
{
message: 'Invalid base 64 string',
validator: (value) => window.atob(value.replace(/^data:.*?;base64,/, '')),
validator: (value) => isValidBase64(value.trim()),
},
],
});

View File

@@ -37,24 +37,20 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
import { withDefaultOnError } from '@/utils/defaults';
import { computed, ref } from 'vue';
const textInput = ref('');
const base64Output = computed(() => window.btoa(textInput.value));
const base64Output = computed(() => textToBase64(textInput.value));
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
const base64Input = ref('');
const textOutput = computed(() => {
try {
return window.atob(base64Input.value);
} catch (_) {
return '';
}
});
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
const b64Validation = useValidation({
source: base64Input,
rules: [{ message: 'Invalid base64 string', validator: (value) => window.atob(value) }],
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
});
</script>

View File

@@ -30,11 +30,12 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { textToBase64 } from '@/utils/base64';
import { computed, ref } from 'vue';
const username = ref('');
const password = ref('');
const header = computed(() => `Authorization: Basic ${window.btoa(`${username.value}:${password.value}`)}`);
const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`);
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
</script>

View File

@@ -59,24 +59,24 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { ref, computed } from 'vue';
import { useValidation } from '@/composable/validation';
import {
entropyToMnemonic,
englishWordList,
chineseSimplifiedWordList,
chineseTraditionalWordList,
czechWordList,
englishWordList,
entropyToMnemonic,
frenchWordList,
generateEntropy,
italianWordList,
japaneseWordList,
koreanWordList,
mnemonicToEntropy,
portugueseWordList,
spanishWordList,
generateEntropy,
mnemonicToEntropy,
} from '@it-tools/bip39';
import { Copy, Refresh } from '@vicons/tabler';
import { useValidation } from '@/composable/validation';
import { computed, ref } from 'vue';
const languages = {
English: englishWordList,
@@ -134,7 +134,7 @@ const mnemonicValidation = useValidation({
{
validator: (value) => {
try {
mnemonicToEntropy(value);
mnemonicToEntropy(value, languages[language.value]);
return true;
} catch (_) {
return false;

View File

@@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest';
import { formatChronometerTime } from './chronometer.service';
import { formatMs } from './chronometer.service';
describe('chronometer', () => {
describe('formatChronometerTime', () => {
it('format the elapsed time', () => {
expect(formatChronometerTime({ elapsed: 123456 })).toEqual('02:03.456');
expect(formatChronometerTime({ elapsed: 123456, msPerUnit: 100 })).toEqual('03:25:45.600');
expect(formatChronometerTime({ elapsed: 12345600 })).toEqual('03:25:45.600');
expect(formatMs(0)).toEqual('00:00.000');
expect(formatMs(1)).toEqual('00:00.001');
expect(formatMs(123456)).toEqual('02:03.456');
expect(formatMs(12345600)).toEqual('03:25:45.600');
});
});
});

View File

@@ -1,10 +1,8 @@
export function formatChronometerTime({ elapsed, msPerUnit = 1 }: { elapsed: number; msPerUnit?: number }) {
const elapsedMs = elapsed * msPerUnit;
const ms = elapsedMs % 1000;
const secs = ((elapsedMs - ms) / 1000) % 60;
const mins = (((elapsedMs - ms) / 1000 - secs) / 60) % 60;
const hrs = (((elapsedMs - ms) / 1000 - secs) / 60 - mins) / 60;
export function formatMs(msTotal: number) {
const ms = msTotal % 1000;
const secs = ((msTotal - ms) / 1000) % 60;
const mins = (((msTotal - ms) / 1000 - secs) / 60) % 60;
const hrs = (((msTotal - ms) / 1000 - secs) / 60 - mins) / 60;
const hrsString = hrs > 0 ? `${hrs.toString().padStart(2, '0')}:` : '';
return `${hrsString}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms

View File

@@ -1,11 +1,11 @@
<template>
<div>
<n-card>
<div class="duration">{{ formatChronometerTime({ elapsed: counter, msPerUnit }) }}</div>
<div class="duration">{{ formatMs(counter) }}</div>
</n-card>
<br />
<n-space justify="center">
<n-button v-if="!isActive" secondary type="primary" @click="resume">Start</n-button>
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
<n-button secondary @click="counter = 0">Reset</n-button>
@@ -14,12 +14,33 @@
</template>
<script setup lang="ts">
import { useInterval } from '@vueuse/core';
import { formatChronometerTime } from './chronometer.service';
import { useRafFn } from '@vueuse/core';
import { ref } from 'vue';
import { formatMs } from './chronometer.service';
const msPerUnit = 10;
const isRunning = ref(false);
const counter = ref(0);
const { counter, pause, resume, isActive } = useInterval(msPerUnit, { controls: true, immediate: false });
let previousRafDate = Date.now();
const { pause: pauseRaf, resume: resumeRaf } = useRafFn(
() => {
const deltaMs = Date.now() - previousRafDate;
previousRafDate = Date.now();
counter.value += deltaMs;
},
{ immediate: false },
);
function resume() {
previousRafDate = Date.now();
resumeRaf();
isRunning.value = true;
}
function pause() {
pauseRaf();
isRunning.value = false;
}
</script>
<style lang="less" scoped>

View File

@@ -49,7 +49,7 @@
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy.js';
import { useCopy } from '@/composable/copy';
import {
enc,
HmacMD5,
@@ -63,7 +63,7 @@ import {
lib,
} from 'crypto-js';
import { computed, ref } from 'vue';
import { convertHexToBin } from '../hash-text/hash-text.service.js';
import { convertHexToBin } from '../hash-text/hash-text.service';
const algos = {
MD5: HmacMD5,

View File

@@ -52,7 +52,8 @@
<script setup lang="ts">
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64.js';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { textToBase64 } from '@/utils/base64';
import { computed, ref } from 'vue';
const width = ref(600);
@@ -75,7 +76,7 @@ const svgString = computed(() => {
</svg>
`.trim();
});
const base64 = computed(() => 'data:image/svg+xml;base64,' + window.btoa(svgString.value));
const base64 = computed(() => 'data:image/svg+xml;base64,' + textToBase64(svgString.value));
const { copy: copySVG } = useCopy({ source: svgString });
const { copy: copyBase64 } = useCopy({ source: base64 });

69
src/utils/base64.test.ts Normal file
View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { base64ToText, isValidBase64, removePotentialDataAndMimePrefix, textToBase64 } from './base64';
describe('base64 utils', () => {
describe('textToBase64', () => {
it('should convert string into base64', () => {
expect(textToBase64('')).to.eql('');
expect(textToBase64('a')).to.eql('YQ==');
expect(textToBase64('lorem ipsum')).to.eql('bG9yZW0gaXBzdW0=');
expect(textToBase64('-1')).to.eql('LTE=');
});
});
describe('base64ToText', () => {
it('should convert base64 into text', () => {
expect(base64ToText('')).to.eql('');
expect(base64ToText('YQ==')).to.eql('a');
expect(base64ToText('bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
expect(base64ToText('LTE=')).to.eql('-1');
});
it('should throw for incorrect base64 string', () => {
expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
expect(() => base64ToText('é')).to.throw('Incorrect base64 string');
// missing final '='
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
});
});
describe('isValidBase64', () => {
it('should return true for correct base64 string', () => {
expect(isValidBase64('')).to.eql(true);
expect(isValidBase64('bG9yZW0gaXBzdW0=')).to.eql(true);
expect(isValidBase64('LTE=')).to.eql(true);
expect(isValidBase64('YQ==')).to.eql(true);
expect(isValidBase64('data:text/plain;base64,YQ==')).to.eql(true);
});
it('should return false for incorrect base64 string', () => {
expect(isValidBase64('a')).to.eql(false);
expect(isValidBase64(' ')).to.eql(false);
expect(isValidBase64('é')).to.eql(false);
expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
// missing final '='
expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
});
it('should return false for untrimmed correct base64 string', () => {
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false);
expect(isValidBase64(' LTE=')).to.eql(false);
expect(isValidBase64(' YQ== ')).to.eql(false);
});
});
describe('removePotentialDataAndMimePrefix', () => {
it('should remove data prefix of string', () => {
expect(removePotentialDataAndMimePrefix('')).to.eql('');
expect(removePotentialDataAndMimePrefix('lorem ipsum')).to.eql('lorem ipsum');
expect(removePotentialDataAndMimePrefix('bG9yZW0gaXBzdW0=')).to.eql('bG9yZW0gaXBzdW0=');
expect(removePotentialDataAndMimePrefix('data:image/jpeg;base64,lorem')).to.eql('lorem');
expect(removePotentialDataAndMimePrefix('data:image/jpeg;notbase64,lorem')).to.eql(
'data:image/jpeg;notbase64,lorem',
);
expect(removePotentialDataAndMimePrefix('data:unknownmime;base64,lorem')).to.eql('lorem');
});
});
});

33
src/utils/base64.ts Normal file
View File

@@ -0,0 +1,33 @@
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
function textToBase64(str: string) {
return window.btoa(str);
}
function base64ToText(str: string) {
if (!isValidBase64(str)) {
throw new Error('Incorrect base64 string');
}
const cleanStr = removePotentialDataAndMimePrefix(str);
try {
return window.atob(cleanStr);
} catch (_) {
throw new Error('Incorrect base64 string');
}
}
function removePotentialDataAndMimePrefix(str: string) {
return str.replace(/^data:.*?;base64,/, '');
}
function isValidBase64(str: string) {
const cleanStr = removePotentialDataAndMimePrefix(str);
try {
return window.btoa(window.atob(cleanStr)) === cleanStr;
} catch (err) {
return false;
}
}

View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import { withDefaultOnError } from './defaults';
describe('defaults util', () => {
describe('withDefaultOnError', () => {
it('should return the callback or the default one if the callback throws', () => {
expect(withDefaultOnError(() => 'original', 'default')).to.eql('original');
});
expect(
withDefaultOnError(() => {
throw '';
}, 'default'),
).to.eql('default');
});
});

9
src/utils/defaults.ts Normal file
View File

@@ -0,0 +1,9 @@
export { withDefaultOnError };
function withDefaultOnError<A, B>(cb: () => A, defaultValue: B): A | B {
try {
return cb();
} catch (_) {
return defaultValue;
}
}