Compare commits

...

18 Commits

Author SHA1 Message Date
Corentin Thomasset
cc717bc87e chore(release): 2.10.0 2022-08-03 17:25:48 +02:00
Corentin Thomasset
1bc6380c6f feat(new-tool): hmac generator 2022-08-03 17:19:53 +02:00
Corentin Thomasset
02c4963531 refactor(lint): externalization of prettier for simpler IDE support 2022-08-03 17:19:53 +02:00
Corentin Thomasset
129f74c371 feat(new-tool): an svg placeholder image generator 2022-08-03 17:18:18 +02:00
Corentin Thomasset
0be33fb337 refactor(display): mutualized code display 2022-08-03 17:18:18 +02:00
Corentin THOMASSET
422b6eb05a feat(hash-text): digest base selector (#254) 2022-08-03 17:16:03 +02:00
Corentin Thomasset
fad4833ca2 chore(deps): updated dependencies 2022-08-03 17:13:34 +02:00
marvin-j97
531a25c1c4 fix(eta-calculator): clamp inputs (#249) 2022-08-03 17:13:33 +02:00
Corentin Thomasset
77b5b0cab5 refactor(validation): simplified validation system 2022-08-03 17:13:33 +02:00
Corentin Thomasset
7570ad9656 fix(bip39-generator): typo in validation message 2022-08-03 17:13:33 +02:00
Corentin Thomasset
8a9e7888de fix(base64-to-string): prevent validation error 2022-08-03 17:13:33 +02:00
Corentin Thomasset
750a76b00f refactor(base64-to-file): clean validation to convert base64 to file 2022-08-03 17:13:33 +02:00
marvin-j97
5f03619ab4 fix(wording): removed spaces before ponctuation (#252) 2022-08-03 08:46:59 +02:00
Corentin Thomasset
352365f012 chore(release): 2.9.2 2022-07-28 19:31:16 +02:00
Corentin Thomasset
4f599b6999 fix(device-information): handle of unknown values 2022-07-28 19:30:36 +02:00
JWB
138149e6f0 fix(device-information): prevent unwanted y-truncature of text
* Device information page styling fix

I was checking out your site, and really like it! already added to my bookmarks, and when I was on the Device Information page I noticed the `p` and `y` in the words 1 dppx and landscape-primary were cut off and my ocd kicked in to tweak it.
The fix was to remove the line height set on the value, and then to keep the spacing how it was just moving it up 5px seemed to do the trick.

I'm using Firefox 102.0.1 on Windows 11. I'll attach some pics so you can see the before and after.

I've never messed with vue, but after looking at this file here ya make me want to check it out more. I love how its keeping it all together in one file.
Might also need to rebuild it the site after this change, but you get the point. 

Awesome site!

* Update src/tools/device-information/device-information.vue

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2022-07-28 19:28:23 +02:00
Corentin Thomasset
412de23796 fix(base64-file): fixed url slug 2022-07-25 23:31:06 +02:00
Corentin Thomasset
1a22d55b3c refactor(base64-file): fixed typo 2022-07-25 23:23:53 +02:00
26 changed files with 3914 additions and 3295 deletions

View File

@@ -16,15 +16,6 @@ module.exports = {
},
rules: {
'vue/multi-word-component-names': ['off'],
'prettier/prettier': [
'error',
{
singleQuote: true,
semi: true,
tabWidth: 2,
trailingComma: 'all',
printWidth: 120,
},
],
'prettier/prettier': ['error'],
},
};

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 120
}

View File

@@ -2,6 +2,45 @@
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.0](https://github.com/CorentinTh/it-tools/compare/v2.9.2...v2.10.0) (2022-08-03)
### Features
* **hash-text:** digest base selector ([#254](https://github.com/CorentinTh/it-tools/issues/254)) ([422b6eb](https://github.com/CorentinTh/it-tools/commit/422b6eb05a2fb5e7eec816a6bd2d37b53e4a6bdc))
* **new-tool:** an svg placeholder image generator ([129f74c](https://github.com/CorentinTh/it-tools/commit/129f74c371eaf09fdc3a19afb709cee40b7aaf7f))
* **new-tool:** hmac generator ([1bc6380](https://github.com/CorentinTh/it-tools/commit/1bc6380c6fdd7a9b500422a54bc508ab5557eb46))
### Bug Fixes
* **base64-to-string:** prevent validation error ([8a9e788](https://github.com/CorentinTh/it-tools/commit/8a9e7888dec41364c8c17b1234adcdc0616612b0))
* **bip39-generator:** typo in validation message ([7570ad9](https://github.com/CorentinTh/it-tools/commit/7570ad965602233f860b9e03177a5b9dacf1b034))
* **eta-calculator:** clamp inputs ([#249](https://github.com/CorentinTh/it-tools/issues/249)) ([531a25c](https://github.com/CorentinTh/it-tools/commit/531a25c1c4892835633ba5635c6ee48e1fbef31c))
* **wording:** removed spaces before ponctuation ([#252](https://github.com/CorentinTh/it-tools/issues/252)) ([5f03619](https://github.com/CorentinTh/it-tools/commit/5f03619ab44c0b35455c46698ec37d79e87555b5))
### Refactors
* **base64-to-file:** clean validation to convert base64 to file ([750a76b](https://github.com/CorentinTh/it-tools/commit/750a76b00fb79c0e9c2851c112141158ee0ffab1))
* **display:** mutualized code display ([0be33fb](https://github.com/CorentinTh/it-tools/commit/0be33fb337e8d82474922c0fdf9555aa328cd729))
* **lint:** externalization of prettier for simpler IDE support ([02c4963](https://github.com/CorentinTh/it-tools/commit/02c49635315661ca08deb0859c5ba33113368b9b))
* **validation:** simplified validation system ([77b5b0c](https://github.com/CorentinTh/it-tools/commit/77b5b0cab50a05dcb419ce87d74517d82e7cd2c0))
### [2.9.2](https://github.com/CorentinTh/it-tools/compare/v2.9.1...v2.9.2) (2022-07-28)
### Bug Fixes
* **base64-file:** fixed url slug ([412de23](https://github.com/CorentinTh/it-tools/commit/412de23796babbc080b0768a75029ff2ddf2acfc))
* **device-information:** handle of unknown values ([4f599b6](https://github.com/CorentinTh/it-tools/commit/4f599b699901a93444bcc67cbb3b3556a0561ae4))
* **device-information:** prevent unwanted y-truncature of text ([138149e](https://github.com/CorentinTh/it-tools/commit/138149e6f0be91255907a6083887898e5c68882e))
### Refactors
* **base64-file:** fixed typo ([1a22d55](https://github.com/CorentinTh/it-tools/commit/1a22d55b3c48f58b05b5a50de4fea260e781fbef))
### [2.9.1](https://github.com/CorentinTh/it-tools/compare/v2.9.0...v2.9.1) (2022-07-25)

6548
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2.9.1",
"version": "2.10.0",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -76,6 +76,7 @@
"@vue/tsconfig": "^0.1.3",
"c8": "^7.11.0",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-vue": "^8.2.0",
"jsdom": "^19.0.0",
"less": "^4.1.2",

View File

@@ -0,0 +1,98 @@
<template>
<div style="overflow-x: hidden; width: 100%">
<n-card class="result-card">
<n-scrollbar
x-scrollable
trigger="none"
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
>
<n-config-provider :hljs="hljs">
<n-code :code="value" :language="language" :trim="false" />
</n-config-provider>
</n-scrollbar>
<n-tooltip v-if="value" trigger="hover">
<template #trigger>
<div class="copy-button" :class="[copyPlacement]">
<n-button secondary circle size="large" @click="onCopyClicked">
<n-icon size="22" :component="Copy" />
</n-button>
</div>
</template>
<span>{{ tooltipText }}</span>
</n-tooltip>
</n-card>
<n-space v-if="copyPlacement === 'outside'" justify="center" style="margin-top: 15px">
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
</n-space>
</div>
</template>
<script setup lang="ts">
import { Copy } from '@vicons/tabler';
import { useClipboard, useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import jsonHljs from 'highlight.js/lib/languages/json';
import sqlHljs from 'highlight.js/lib/languages/sql';
import { ref, toRefs } from 'vue';
hljs.registerLanguage('sql', sqlHljs);
hljs.registerLanguage('json', jsonHljs);
const props = withDefaults(
defineProps<{
value: string;
followHeightOf?: HTMLElement | null;
language?: string;
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none';
copyMessage?: string;
}>(),
{
followHeightOf: null,
language: 'txt',
copyPlacement: 'top-right',
copyMessage: 'Copy to clipboard',
},
);
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
const { copy } = useClipboard({ source: value });
const tooltipText = ref(copyMessage.value);
function onCopyClicked() {
copy();
tooltipText.value = 'Copied !';
setTimeout(() => {
tooltipText.value = copyMessage.value;
}, 2000);
}
</script>
<style lang="less" scoped>
::v-deep(.n-scrollbar) {
padding-bottom: 10px;
margin-bottom: -10px;
}
.result-card {
position: relative;
.copy-button {
position: absolute;
opacity: 1;
&.top-right {
top: 10px;
right: 10px;
}
&.bottom-right {
bottom: 10px;
right: 10px;
}
&.outside,
&.none {
display: none;
}
}
}
</style>

View File

@@ -1,16 +1,35 @@
import { extension as getExtensionFromMime } from 'mime-types';
import type { Ref } from 'vue';
function getFileExtensionFromBase64({
base64String,
defaultExtension = 'txt',
}: {
base64String: string;
defaultExtension?: string;
}) {
const hasMimeType = base64String.match(/data:(.*?);base64/i);
if (hasMimeType) {
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
}
return defaultExtension;
}
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
return {
download() {
const base64 = source.value;
const mimeType = base64.match(/data:(.*?);base64/i)?.[1] ?? 'text/plain';
console.log({ mimeType });
const cleanFileName = filename ?? `file.${getExtensionFromMime(mimeType)}`;
const base64String = source.value;
if (base64String === '') {
throw new Error('Base64 string is empty');
}
const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`;
const a = document.createElement('a');
a.href = source.value;
a.href = base64String;
a.download = cleanFileName;
a.click();
},

View File

@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { describe, expect, it } from 'vitest';
import { isFalsyOrHasThrown } from './validation';
describe('useValidation', () => {
describe('isFalsyOrHasThrown', () => {
it('should return true if the callback return nil, false or throw', () => {
expect(isFalsyOrHasThrown(() => false)).toBe(true);
expect(isFalsyOrHasThrown(() => null)).toBe(true);
expect(isFalsyOrHasThrown(() => undefined)).toBe(true);
expect(isFalsyOrHasThrown(() => {})).toBe(true);
expect(
isFalsyOrHasThrown(() => {
throw new Error();
}),
).toBe(true);
});
it('should return true for any truthy values and empty string and 0 values', () => {
expect(isFalsyOrHasThrown(() => true)).toBe(false);
expect(isFalsyOrHasThrown(() => 'string')).toBe(false);
expect(isFalsyOrHasThrown(() => 1)).toBe(false);
expect(isFalsyOrHasThrown(() => 0)).toBe(false);
expect(isFalsyOrHasThrown(() => '')).toBe(false);
expect(isFalsyOrHasThrown(() => [])).toBe(false);
expect(isFalsyOrHasThrown(() => ({}))).toBe(false);
});
});
});

View File

@@ -1,38 +1,65 @@
import _ from 'lodash';
import { reactive, watch, type Ref } from 'vue';
type UseValidationRule<T> = {
validator: (value: T) => boolean;
message: string;
};
type ValidatorReturnType = unknown;
function isFalsyOrHasThrown(cb: () => boolean) {
interface UseValidationRule<T> {
validator: (value: T) => ValidatorReturnType;
message: string;
}
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
try {
return !cb();
const returnValue = cb();
if (_.isNil(returnValue)) return true;
return returnValue === false;
} catch (_) {
return true;
}
}
type ValidationAttrs = {
feedback: string;
validationStatus: string | undefined;
};
export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: UseValidationRule<T>[] }) {
const state = reactive<{
message: string;
status: undefined | 'error';
isValid: boolean;
attrs: ValidationAttrs;
}>({
message: '',
status: undefined,
isValid: false,
attrs: {
validationStatus: undefined,
feedback: '',
},
});
watch([source], () => {
state.message = '';
state.status = undefined;
watch(
[source],
() => {
state.message = '';
state.status = undefined;
for (const rule of rules) {
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
state.message = rule.message;
state.status = 'error';
for (const rule of rules) {
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
state.message = rule.message;
state.status = 'error';
}
}
}
});
state.isValid = state.status !== 'error';
state.attrs.feedback = state.message;
state.attrs.validationStatus = state.status;
},
{ immediate: true },
);
return state;
}

View File

@@ -47,7 +47,7 @@ useHead({ title: 'About - IT Tools' });
file of the repository.
</n-p>
<n-h2>Found a bug ? A tool is missing ?</n-h2>
<n-h2>Found a bug? A tool is missing?</n-h2>
<n-p>
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
feature request in the

View File

@@ -1,8 +1,16 @@
<template>
<n-card title="Base64 to file">
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
<n-form-item
:feedback="base64InputValidation.message"
:validation-status="base64InputValidation.status"
:show-label="false"
>
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
</n-form-item>
<n-space justify="center">
<n-button secondary @click="download()"> Download file </n-button>
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
Download file
</n-button>
</n-space>
</n-card>
@@ -16,7 +24,7 @@
</n-upload-dragger>
</n-upload>
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in ase64 will be here" />
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
<n-space justify="center">
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
</n-space>
@@ -26,6 +34,7 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { useValidation } from '@/composable/validation';
import { Upload } from '@vicons/tabler';
import { useBase64 } from '@vueuse/core';
import type { UploadFileInfo } from 'naive-ui';
@@ -33,6 +42,25 @@ import { ref, type Ref } from 'vue';
const base64Input = ref('');
const { download } = useDownloadFileFromBase64({ source: base64Input });
const base64InputValidation = useValidation({
source: base64Input,
rules: [
{
message: 'Invalid base 64 string',
validator: (value) => window.atob(value.replace(/^data:.*?;base64,/, '')),
},
],
});
function downloadFile() {
if (!base64InputValidation.isValid) return;
try {
download();
} catch (_) {
//
}
}
const fileList = ref();
const fileInput = ref() as Ref<File>;

View File

@@ -3,10 +3,9 @@ import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Base64 file converter',
path: '/base64-converter',
path: '/base64-file-converter',
description: "Convert string, files or images into a it's base64 representation.",
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
component: () => import('./base64-file-converter.vue'),
icon: FileDigit,
redirectFrom: ['/file-to-base64', '/base64-string-converter'],
});

View File

@@ -20,11 +20,7 @@
</n-card>
<n-card title="Base64 to string">
<n-form-item
label="Base64 string to decode"
:validation-status="b64Validation.status"
:feedback="b64Validation.message"
>
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
</n-form-item>
@@ -58,7 +54,7 @@ const textOutput = computed(() => {
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
const b64Validation = useValidation({
source: base64Input,
rules: [{ message: 'Invalid base64 string', validator: (value) => true || window.atob(value) }],
rules: [{ message: 'Invalid base64 string', validator: (value) => window.atob(value) }],
});
</script>

View File

@@ -123,7 +123,7 @@ const entropyValidation = useValidation({
},
{
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
message: 'Entropy should an hexadecimal number',
message: 'Entropy should be an hexadecimal string',
},
],
});

View File

@@ -1,16 +1,17 @@
<template>
<n-card v-for="{ name, information } in sections" :key="name" :title="name">
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
<n-gi v-for="{ label, value } in information" :key="label" class="information">
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
<n-card :bordered="false" embedded>
<div class="label">
{{ label }}
</div>
<div class="value">
<n-ellipsis>
{{ value.value }}
<n-ellipsis v-if="value">
{{ value }}
</n-ellipsis>
<div v-else class="undefined-value">unknown</div>
</div>
</n-card>
</n-gi>
@@ -89,7 +90,10 @@ const sections = [
.value {
font-size: 20px;
font-weight: 400;
line-height: 1;
}
.undefined-value {
opacity: 0.8;
}
}
</style>

View File

@@ -11,7 +11,7 @@
<div>
<n-space item-style="flex:1 1 0">
<n-form-item label="Amount of element to consume">
<n-input-number v-model:value="unitCount" />
<n-input-number v-model:value="unitCount" :min="1" />
</n-form-item>
<n-form-item label="The consumption started at">
<n-date-picker v-model:value="startedAt" type="datetime" />
@@ -19,10 +19,10 @@
</n-space>
<n-form-item label="Amount of unit consumed by time span" :show-feedback="false">
<n-input-number v-model:value="unitPerTimeSpan" />
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
<span style="margin: 0 10px">in</span>
<n-input-group>
<n-input-number v-model:value="timeSpan" />
<n-input-number v-model:value="timeSpan" :min="1" />
<n-select
v-model:value="timeSpanUnitMultiplier"
:options="[

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';
import { convertHexToBin } from './hash-text.service';
describe('hash text', () => {
describe('convertHexToBin', () => {
it('convert hex to bin', () => {
expect(convertHexToBin('')).toEqual('');
expect(convertHexToBin('FF')).toEqual('11111111');
expect(convertHexToBin('F'.repeat(200))).toEqual('1111'.repeat(200));
expect(convertHexToBin('2123006AD00F694CE120')).toEqual(
'00100001001000110000000001101010110100000000111101101001010011001110000100100000',
);
});
});
});

View File

@@ -0,0 +1,7 @@
export function convertHexToBin(hex: string) {
return hex
.trim()
.split('')
.map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0'))
.join('');
}

View File

@@ -1,10 +1,34 @@
<template>
<div>
<n-card>
<n-input v-model:value="clearText" type="textarea" placeholder="Your string..." :autosize="{ minRows: 3 }" />
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
<n-divider />
<n-form-item label="Digest encoding">
<n-select
v-model:value="encoding"
:options="[
{
label: 'Binary (base 2)',
value: 'Bin',
},
{
label: 'Hexadecimal (base 16)',
value: 'Hex',
},
{
label: 'Base64 (base 64)',
value: 'Base64',
},
{
label: 'Base64url (base 64 with url safe chars)',
value: 'Base64url',
},
]"
/>
</n-form-item>
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label>
@@ -16,9 +40,10 @@
</template>
<script setup lang="ts">
import InputCopyable from '../../components/InputCopyable.vue';
import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js';
import { ref } from 'vue';
import { MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3, RIPEMD160 } from 'crypto-js';
import InputCopyable from '../../components/InputCopyable.vue';
import { convertHexToBin } from './hash-text.service';
const algos = {
MD5,
@@ -32,10 +57,18 @@ const algos = {
} as const;
type AlgoNames = keyof typeof algos;
type Encoding = keyof typeof enc | 'Bin';
const algoNames = Object.keys(algos) as AlgoNames[];
const encoding = ref<Encoding>('Hex');
const clearText = ref('');
const clearText = ref(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lacus metus blandit dolor lacus natoque ad fusce aliquam velit.',
);
const hashText = (algo: AlgoNames, value: string) => algos[algo](value).toString();
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
if (encoding === 'Bin') {
return convertHexToBin(words.toString(enc.Hex));
}
return words.toString(enc[encoding]);
}
const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value);
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div>
<n-form-item label="Plain text to compute the hash">
<n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." />
</n-form-item>
<n-form-item label="Secret key">
<n-input v-model:value="secret" placeholder="Enter the secret key..." />
</n-form-item>
<n-space item-style="flex:1 1 0">
<n-form-item label="Hashing function">
<n-select
v-model:value="hashFunction"
placeholder="Select an hashing function..."
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
/>
</n-form-item>
<n-form-item label="Output encoding">
<n-select
v-model:value="encoding"
placeholder="Select the result encoding..."
:options="[
{
label: 'Binary (base 2)',
value: 'Bin',
},
{
label: 'Hexadecimal (base 16)',
value: 'Hex',
},
{
label: 'Base64 (base 64)',
value: 'Base64',
},
{
label: 'Base64-url (base 64 with url safe chars)',
value: 'Base64url',
},
]"
/>
</n-form-item>
</n-space>
<n-form-item label="HMAC of your text">
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copy()">Copy HMAC</n-button>
</n-space>
</div>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy.js';
import {
enc,
HmacMD5,
HmacRIPEMD160,
HmacSHA1,
HmacSHA224,
HmacSHA256,
HmacSHA3,
HmacSHA384,
HmacSHA512,
lib,
} from 'crypto-js';
import { computed, ref } from 'vue';
import { convertHexToBin } from '../hash-text/hash-text.service.js';
const algos = {
MD5: HmacMD5,
RIPEMD160: HmacRIPEMD160,
SHA1: HmacSHA1,
SHA3: HmacSHA3,
SHA224: HmacSHA224,
SHA256: HmacSHA256,
SHA384: HmacSHA384,
SHA512: HmacSHA512,
} as const;
type Encoding = keyof typeof enc | 'Bin';
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
if (encoding === 'Bin') {
return convertHexToBin(words.toString(enc.Hex));
}
return words.toString(enc[encoding]);
}
const plainText = ref('');
const secret = ref('');
const hashFunction = ref<keyof typeof algos>('SHA256');
const encoding = ref<Encoding>('Hex');
const hmac = computed(() =>
formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value),
);
const { copy } = useCopy({ source: hmac });
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,12 @@
import { ShortTextRound } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Hmac generator',
path: '/hmac-generator',
description:
'Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.',
keywords: ['hmac', 'generator', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160'],
component: () => import('./hmac-generator.vue'),
icon: ShortTextRound,
});

View File

@@ -16,6 +16,7 @@ import { tool as cypher } from './encryption';
import { tool as etaCalculator } from './eta-calculator';
import { tool as gitMemo } from './git-memo';
import { tool as hashText } from './hash-text';
import { tool as hmacGenerator } from './hmac-generator';
import { tool as htmlEntities } from './html-entities';
import { tool as baseConverter } from './integer-base-converter';
import { tool as jsonViewer } from './json-viewer';
@@ -25,6 +26,7 @@ import { tool as qrCodeGenerator } from './qr-code-generator';
import { tool as randomPortGenerator } from './random-port-generator';
import { tool as romanNumeralConverter } from './roman-numeral-converter';
import { tool as sqlPrettify } from './sql-prettify';
import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator';
import { tool as textStatistics } from './text-statistics';
import { tool as tokenGenerator } from './token-generator';
import { tool as urlEncoder } from './url-encoder';
@@ -35,7 +37,7 @@ export const toolsByCategory: ToolCategory[] = [
{
name: 'Crypto',
icon: LockOpen,
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39],
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator],
},
{
name: 'Converter',
@@ -53,7 +55,12 @@ export const toolsByCategory: ToolCategory[] = [
{
name: 'Web',
icon: LockOpen,
components: [urlEncoder, htmlEntities, qrCodeGenerator, urlParser, deviceInformation, basicAuthGenerator],
components: [urlEncoder, htmlEntities, urlParser, deviceInformation, basicAuthGenerator],
},
{
name: 'Images',
icon: LockOpen,
components: [qrCodeGenerator, svgPlaceholderGenerator],
},
{
name: 'Development',

View File

@@ -17,26 +17,16 @@
/>
</n-form-item>
<n-form-item label="Prettify version of your json">
<n-card class="result-card" :style="`min-height: ${inputElementHeight ?? 400}px`">
<n-config-provider :hljs="hljs">
<n-code :code="cleanJson" language="json" :trim="false" />
</n-config-provider>
<n-button v-if="cleanJson" class="copy-button" secondary @click="copy">Copy</n-button>
</n-card>
<textarea-copyable :value="cleanJson" language="json" :follow-height-of="inputElement" />
</n-form-item>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useValidation } from '@/composable/validation';
import { useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import { computed, ref } from 'vue';
hljs.registerLanguage('json', json);
const inputElement = ref<HTMLElement>();
const { height: inputElementHeight } = useElementSize(inputElement);
const rawJson = ref('{"hello": "world"}');
const cleanJson = computed(() => {
@@ -47,8 +37,6 @@ const cleanJson = computed(() => {
}
});
const { copy } = useCopy({ source: cleanJson });
const rawJsonValidation = useValidation({
source: rawJson,
rules: [

View File

@@ -58,27 +58,17 @@
/>
</n-form-item>
<n-form-item label="Prettify version of your query">
<n-card class="result-card" :style="`min-height: ${inputElementHeight ?? 400}px`">
<n-config-provider :hljs="hljs">
<n-code :code="prettySQL" language="sql" :trim="false" />
</n-config-provider>
<n-button v-if="prettySQL" class="copy-button" secondary @click="copy">Copy</n-button>
</n-card>
<textarea-copyable :value="prettySQL" language="sql" :follow-height-of="inputElement" />
</n-form-item>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useStyleStore } from '@/stores/style.store';
import { useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import sqlHljs from 'highlight.js/lib/languages/sql';
import { format as formatSQL, type FormatFnOptions } from 'sql-formatter';
import { computed, reactive, ref } from 'vue';
hljs.registerLanguage('sql', sqlHljs);
const inputElement = ref<HTMLElement>();
const { height: inputElementHeight } = useElementSize(inputElement);
const styleStore = useStyleStore();
const config = reactive<Partial<FormatFnOptions>>({
keywordCase: 'upper',
@@ -90,7 +80,6 @@ const config = reactive<Partial<FormatFnOptions>>({
const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;');
const prettySQL = computed(() => formatSQL(rawSQL.value, config));
const { copy } = useCopy({ source: prettySQL });
</script>
<style lang="less" scoped>

View File

@@ -0,0 +1,11 @@
import { ImageOutlined } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'SVG placeholder generator',
path: '/svg-placeholder-generator',
description: 'Generate svg images to use as placeholder in your applications.',
keywords: ['svg', 'placeholder', 'generator', 'image', 'size', 'mockup'],
component: () => import('./svg-placeholder-generator.vue'),
icon: ImageOutlined,
});

View File

@@ -0,0 +1,89 @@
<template>
<div>
<n-form label-placement="left" label-width="100">
<n-space item-style="flex:1 1 0">
<n-form-item label="Width (in px)">
<n-input-number v-model:value="width" placeholder="SVG width..." min="1" />
</n-form-item>
<n-form-item label="Background">
<n-color-picker v-model:value="bgColor" :modes="['hex']" />
</n-form-item>
</n-space>
<n-space item-style="flex:1 1 0">
<n-form-item label="Height (in px)">
<n-input-number v-model:value="height" placeholder="SVG height..." min="1" />
</n-form-item>
<n-form-item label="Text color">
<n-color-picker v-model:value="fgColor" :modes="['hex']" />
</n-form-item>
</n-space>
<n-space item-style="flex:1 1 0">
<n-form-item label="Font size">
<n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" />
</n-form-item>
<n-form-item label="Custom text">
<n-input v-model:value="customText" :placeholder="`Default is ${width}x${height}`" />
</n-form-item>
</n-space>
<n-form-item label="Use exact size" label-placement="left">
<n-switch v-model:value="useExactSize" />
</n-form-item>
</n-form>
<n-form-item label="SVG HTML element">
<textarea-copyable :value="svgString" copy-placement="none" />
</n-form-item>
<n-form-item label="SVG in Base64">
<textarea-copyable :value="base64" copy-placement="none" />
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copySVG()">Copy svg</n-button>
<n-button secondary @click="copyBase64()">Copy base64</n-button>
<n-button secondary @click="download()">Download svg</n-button>
</n-space>
</div>
<n-space vertical justify="start">
<img :src="base64" alt="Image" />
</n-space>
</template>
<script setup lang="ts">
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64.js';
import { computed, ref } from 'vue';
const width = ref(600);
const height = ref(350);
const fontSize = ref(26);
const bgColor = ref('#cccccc');
const fgColor = ref('#333333');
const useExactSize = ref(true);
const customText = ref('');
const svgString = computed(() => {
const w = width.value;
const h = height.value;
const text = customText.value.length > 0 ? customText.value : `${w}x${h}`;
const size = useExactSize.value ? ` width="${w}" height="${h}"` : '';
return `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}>
<rect width="${w}" height="${h}" fill="${bgColor.value}"></rect>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text>
</svg>
`.trim();
});
const base64 = computed(() => 'data:image/svg+xml;base64,' + window.btoa(svgString.value));
const { copy: copySVG } = useCopy({ source: svgString });
const { copy: copyBase64 } = useCopy({ source: base64 });
const { download } = useDownloadFileFromBase64({ source: base64 });
</script>
<style lang="less" scoped>
.n-input-number {
width: 100%;
}
</style>