mirror of
https://github.com/CorentinTh/it-tools.git
synced 2024-09-11 13:37:53 +03:00
Compare commits
8 Commits
2af6527124
...
d92a285cb2
Author | SHA1 | Date | |
---|---|---|---|
|
d92a285cb2 | ||
|
4c014a63e1 | ||
|
dbfe5113b0 | ||
|
e32d38c057 | ||
|
76a19d218d | ||
|
b430baef40 | ||
|
dd4b7e687b | ||
|
30144aa3f5 |
4
components.d.ts
vendored
4
components.d.ts
vendored
@ -119,6 +119,7 @@ declare module '@vue/runtime-core' {
|
||||
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
|
||||
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
|
||||
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||
MarkdownTocGenerator: typeof import('./src/tools/markdown-toc-generator/markdown-toc-generator.vue')['default']
|
||||
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
|
||||
@ -129,15 +130,18 @@ declare module '@vue/runtime-core' {
|
||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLabel: typeof import('naive-ui')['NLabel']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
|
@ -64,6 +64,7 @@
|
||||
"highlight.js": "^11.7.0",
|
||||
"iarna-toml-esm": "^3.0.5",
|
||||
"ibantools": "^4.3.3",
|
||||
"js-base64": "^3.7.6",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
@ -137,5 +138,6 @@
|
||||
"vitest": "^0.34.0",
|
||||
"workbox-window": "^7.0.0",
|
||||
"zx": "^7.2.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@8.15.3"
|
||||
}
|
||||
|
@ -92,6 +92,9 @@ dependencies:
|
||||
ibantools:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
js-base64:
|
||||
specifier: ^3.7.6
|
||||
version: 3.7.7
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
@ -3351,7 +3354,7 @@ packages:
|
||||
dependencies:
|
||||
'@unhead/dom': 0.5.1
|
||||
'@unhead/schema': 0.5.1
|
||||
'@vueuse/shared': 10.7.2(vue@3.3.4)
|
||||
'@vueuse/shared': 10.11.0(vue@3.3.4)
|
||||
unhead: 0.5.1
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
@ -3984,19 +3987,19 @@ packages:
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared@10.3.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
|
||||
/@vueuse/shared@10.11.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
|
||||
dependencies:
|
||||
vue-demi: 0.14.5(vue@3.3.4)
|
||||
vue-demi: 0.14.8(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared@10.7.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==}
|
||||
/@vueuse/shared@10.3.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
|
||||
dependencies:
|
||||
vue-demi: 0.14.6(vue@3.3.4)
|
||||
vue-demi: 0.14.5(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
@ -6472,6 +6475,10 @@ packages:
|
||||
hasBin: 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:
|
||||
resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -9151,8 +9158,8 @@ packages:
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/vue-demi@0.14.6(vue@3.3.4):
|
||||
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
||||
/vue-demi@0.14.8(vue@3.3.4):
|
||||
resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
@ -9442,6 +9449,7 @@ packages:
|
||||
|
||||
/workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
dependencies:
|
||||
workbox-background-sync: 7.0.0
|
||||
workbox-core: 7.0.0
|
||||
|
@ -48,7 +48,7 @@ const output = computed(() => transformer.value(input.value));
|
||||
monospace
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div overflow-auto>
|
||||
<div mb-5px>
|
||||
{{ outputLabel }}
|
||||
</div>
|
||||
|
21
src/composable/debouncedref.ts
Normal file
21
src/composable/debouncedref.ts
Normal 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;
|
@ -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 _ from 'lodash';
|
||||
|
||||
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
|
||||
export {
|
||||
getMimeTypeFromBase64,
|
||||
getMimeTypeFromExtension, getExtensionFromMimeType,
|
||||
useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
|
||||
previewImageFromBase64,
|
||||
};
|
||||
|
||||
const commonMimeTypesSignatures = {
|
||||
'JVBERi0': 'application/pdf',
|
||||
@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
|
||||
defaultExtension?: string
|
||||
}) {
|
||||
if (mimeType) {
|
||||
return getExtensionFromMime(mimeType) ?? defaultExtension;
|
||||
return getExtensionFromMimeType(mimeType) ?? 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 {
|
||||
download() {
|
||||
if (source.value === '') {
|
||||
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();
|
||||
downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useRouteQuery } from '@vueuse/router';
|
||||
import { computed } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
|
||||
export { useQueryParam };
|
||||
export { useQueryParam, useQueryParamOrStorage };
|
||||
|
||||
const transformers = {
|
||||
number: {
|
||||
@ -16,6 +17,12 @@ const transformers = {
|
||||
fromQuery: (value: string) => value.toLowerCase() === 'true',
|
||||
toQuery: (value: boolean) => (value ? 'true' : 'false'),
|
||||
},
|
||||
object: {
|
||||
fromQuery: (value: string) => {
|
||||
return JSON.parse(value);
|
||||
},
|
||||
toQuery: (value: object) => JSON.stringify(value),
|
||||
},
|
||||
};
|
||||
|
||||
function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
|
||||
@ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
|
||||
const type = typeof defaultValue;
|
||||
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
|
||||
|
||||
const storageRef = useStorage(storageName, defaultValue);
|
||||
const proxyDefaultValue = transformer.toQuery(defaultValue as never);
|
||||
const proxy = useRouteQuery(name, proxyDefaultValue);
|
||||
|
||||
const r = ref(defaultValue);
|
||||
|
||||
watch(r,
|
||||
(value) => {
|
||||
proxy.value = transformer.toQuery(value as never);
|
||||
storageRef.value = value as never;
|
||||
},
|
||||
{ deep: true });
|
||||
|
||||
r.value = (proxy.value && proxy.value !== proxyDefaultValue
|
||||
? transformer.fromQuery(proxy.value) as unknown as T
|
||||
: storageRef.value as T) as never;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
@ -2,12 +2,19 @@
|
||||
import { useBase64 } from '@vueuse/core';
|
||||
import type { Ref } from 'vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isValidBase64 } from '@/utils/base64';
|
||||
|
||||
const fileName = ref('file');
|
||||
const fileExtension = ref('');
|
||||
const base64Input = ref('');
|
||||
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
||||
const { download } = useDownloadFileFromBase64Refs(
|
||||
{
|
||||
source: base64Input,
|
||||
filename: fileName,
|
||||
extension: fileExtension,
|
||||
});
|
||||
const base64InputValidation = useValidation({
|
||||
source: base64Input,
|
||||
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() {
|
||||
if (!base64InputValidation.isValid) {
|
||||
return;
|
||||
@ -44,6 +80,24 @@ async function onUpload(file: File) {
|
||||
|
||||
<template>
|
||||
<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
|
||||
v-model:value="base64Input"
|
||||
multiline
|
||||
@ -53,7 +107,14 @@ async function onUpload(file: File) {
|
||||
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()">
|
||||
Download file
|
||||
</c-button>
|
||||
|
@ -4,6 +4,7 @@ import emojiKeywords from 'emojilib';
|
||||
import _ from 'lodash';
|
||||
import type { EmojiInfo } from './emoji.types';
|
||||
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 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 }))
|
||||
.value();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const searchQuery = useDebouncedRef('', 500);
|
||||
|
||||
const { searchResult } = useFuzzySearch({
|
||||
search: searchQuery,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as markdownTocGenerator } from './markdown-toc-generator';
|
||||
|
||||
import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
||||
|
||||
@ -107,6 +108,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
listConverter,
|
||||
tomlToJson,
|
||||
tomlToYaml,
|
||||
markdownTocGenerator,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -39,7 +39,7 @@ const validation = useValidation({
|
||||
{{ section.title }}
|
||||
</th>
|
||||
<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>
|
||||
{{ claim }}
|
||||
</span>
|
||||
@ -47,7 +47,7 @@ const validation = useValidation({
|
||||
({{ claimDescription }})
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<td style="word-wrap: break-word;word-break: break-all;">
|
||||
<span>{{ value }}</span>
|
||||
<span v-if="friendlyValue" ml-2 op-70>
|
||||
({{ friendlyValue }})
|
||||
|
12
src/tools/markdown-toc-generator/index.ts
Normal file
12
src/tools/markdown-toc-generator/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Table } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Markdown toc generator',
|
||||
path: '/markdown-toc-generator',
|
||||
description: 'Generate a TOC from a markdown file/content',
|
||||
keywords: ['markdown', 'toc', 'generator'],
|
||||
component: () => import('./markdown-toc-generator.vue'),
|
||||
icon: Table,
|
||||
createdAt: new Date('2024-05-11'),
|
||||
});
|
6
src/tools/markdown-toc-generator/markdown-contents.d.ts
vendored
Normal file
6
src/tools/markdown-toc-generator/markdown-contents.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module 'markdown-contents'{
|
||||
declare class MarkdownContents {
|
||||
markdown(): string;
|
||||
}
|
||||
export default function Create(markdown: string):MarkdownContents;
|
||||
}
|
@ -0,0 +1,380 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getTocMarkdown,
|
||||
} from './markdown-toc-generator.service';
|
||||
|
||||
describe('markdown-toc-generator', () => {
|
||||
it('Generate TOC correctly', async () => {
|
||||
expect(getTocMarkdown({
|
||||
markdown: '',
|
||||
})).to.equal('');
|
||||
|
||||
const sourceMarkdown = `# Some main title
|
||||
|
||||
[TOC]
|
||||
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
## Last Title`;
|
||||
|
||||
expect(getTocMarkdown({
|
||||
markdown: sourceMarkdown,
|
||||
anchorPrefix: 'h-',
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#h-first-title)
|
||||
- [Second Spaced Title](#h-second-spaced-title)
|
||||
* [Title with Link TOC](#h-title-with-link-toc)
|
||||
* [Title with code \`var\`](#h-title-with-code-var)
|
||||
- [Last Title](#h-last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="last-title"></a>
|
||||
## Last Title`);
|
||||
expect(getTocMarkdown({
|
||||
markdown: sourceMarkdown,
|
||||
maxLevel: 2,
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#first-title)
|
||||
- [Second Spaced Title](#second-spaced-title)
|
||||
- [Last Title](#last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="last-title"></a>
|
||||
## Last Title`);
|
||||
expect(getTocMarkdown({
|
||||
markdown: sourceMarkdown,
|
||||
commentStyle: 'liquid',
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
{%- # TOC START -%}
|
||||
- [First Title](#first-title)
|
||||
- [Second Spaced Title](#second-spaced-title)
|
||||
* [Title with Link TOC](#title-with-link-toc)
|
||||
* [Title with code \`var\`](#title-with-code-var)
|
||||
- [Last Title](#last-title)
|
||||
{%- # TOC END -%}
|
||||
|
||||
{%- # TOC ANCHOR -%}<a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
{%- # TOC ANCHOR -%}<a name="second-spaced-title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
{%- # TOC ANCHOR -%}<a name="title-with-link-toc"></a>
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
{%- # TOC ANCHOR -%}<a name="title-with-code-var"></a>
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
{%- # TOC ANCHOR -%}<a name="last-title"></a>
|
||||
## Last Title`);
|
||||
expect(getTocMarkdown({
|
||||
markdown: sourceMarkdown,
|
||||
generateAnchors: false,
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#first-title)
|
||||
- [Second Spaced Title](#second-spaced-title)
|
||||
* [Title with Link TOC](#title-with-link-toc)
|
||||
* [Title with code \`var\`](#title-with-code-var)
|
||||
- [Last Title](#last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
## Last Title`);
|
||||
expect(getTocMarkdown({
|
||||
markdown: sourceMarkdown,
|
||||
indentSpaces: 4,
|
||||
indentChars: '-',
|
||||
concatSpaces: false,
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#first-title)
|
||||
- [Second Spaced Title](#second--spaced--title)
|
||||
- [Title with Link TOC](#title-with-link-toc)
|
||||
- [Title with code \`var\`](#title-with-code-var)
|
||||
- [Last Title](#last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="second--spaced--title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="last-title"></a>
|
||||
## Last Title`);
|
||||
});
|
||||
|
||||
it('Regenerate TOC correctly', async () => {
|
||||
expect(getTocMarkdown({
|
||||
markdown: `# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#first-title)
|
||||
- [Second Spaced Title](#second--spaced--title)
|
||||
- [Title with Link TOC](#title-with-link-toc)
|
||||
- [Title with code \`var\`](#title-with-code-var)
|
||||
- [Last Title](#last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="second--spaced--title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="last-title"></a>
|
||||
## Last Title`,
|
||||
anchorPrefix: 'h-',
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [First Title](#h-first-title)
|
||||
- [Second Spaced Title](#h-second-spaced-title)
|
||||
* [Title with Link TOC](#h-title-with-link-toc)
|
||||
* [Title with code \`var\`](#h-title-with-code-var)
|
||||
- [Last Title](#h-last-title)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="first-title"></a>
|
||||
## First Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
|
||||
## Second Spaced Title
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
|
||||
### Title with Link [TOC](http://it-tools.tech)
|
||||
|
||||
\`\`\`
|
||||
## some bash code
|
||||
echo 'test';
|
||||
\`\`\`
|
||||
|
||||
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
|
||||
### Title with code \`var\`
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="last-title"></a>
|
||||
## Last Title`);
|
||||
});
|
||||
|
||||
it('Generate distinct TOC ids', async () => {
|
||||
expect(getTocMarkdown({
|
||||
markdown: `# Some main title
|
||||
|
||||
[TOC]
|
||||
|
||||
## Same Title 1
|
||||
|
||||
Some text
|
||||
|
||||
## Same Title 1
|
||||
|
||||
Some text
|
||||
|
||||
### Same title 1
|
||||
|
||||
Some text`,
|
||||
anchorPrefix: 'h-',
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [Same Title 1](#h-same-title-1)
|
||||
- [Same Title 1](#h-same-title-1-1)
|
||||
* [Same title 1](#h-same-title-1-2)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="same-title-1"></a>
|
||||
## Same Title 1
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="same-title-1-1"></a>
|
||||
## Same Title 1
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="same-title-1-2"></a>
|
||||
### Same title 1
|
||||
|
||||
Some text`);
|
||||
});
|
||||
|
||||
it('Generate ids for non latin', async () => {
|
||||
expect(getTocMarkdown({
|
||||
markdown: `# Some main title
|
||||
|
||||
[TOC]
|
||||
|
||||
## Привет non-latin 你好
|
||||
|
||||
Some text
|
||||
|
||||
## 😄 emoji
|
||||
|
||||
Some text
|
||||
|
||||
### Other title 1
|
||||
|
||||
Some text`,
|
||||
anchorPrefix: 'h-',
|
||||
})).to.equal(`# Some main title
|
||||
|
||||
<!-- TOC START -->
|
||||
- [Привет non-latin 你好](#h--non-latin-)
|
||||
- [😄 emoji](#h--emoji)
|
||||
* [Other title 1](#h-other-title-1)
|
||||
<!-- TOC END -->
|
||||
|
||||
<!-- TOC ANCHOR --><a name="-non-latin-"></a>
|
||||
## Привет non-latin 你好
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="-emoji"></a>
|
||||
## 😄 emoji
|
||||
|
||||
Some text
|
||||
|
||||
<!-- TOC ANCHOR --><a name="other-title-1"></a>
|
||||
### Other title 1
|
||||
|
||||
Some text`);
|
||||
});
|
||||
});
|
@ -0,0 +1,178 @@
|
||||
function stripNonLatinCharacters(text: string) {
|
||||
return text.replace(/[^A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u02BB\u02EE\uA78C\d\s_-]/g, '');
|
||||
};
|
||||
|
||||
function transformInlineCode(text: string, transform: (s: string) => string) {
|
||||
return text.replace(/`(.*?)`/g, (_, p) => {
|
||||
return `\`${transform(p)}\``;
|
||||
});
|
||||
};
|
||||
|
||||
function spacesToDash(text: string) {
|
||||
return text.replace(/\s/g, '-');
|
||||
};
|
||||
|
||||
function stripHtmlTags(text: string) {
|
||||
return text.replace(/<.*?>/g, '');
|
||||
};
|
||||
|
||||
function stripMarkdownLinks(text: string, replacement: string = '$1') {
|
||||
return text.replace(/\[([^\]]*)\]\([^\)]*\)/g, replacement);
|
||||
};
|
||||
|
||||
function concatDashes(text: string) {
|
||||
return text.replace(/--+/g, '-');
|
||||
};
|
||||
|
||||
function removeUnderscoreBoldAndItalics(text: string) {
|
||||
const underscoreBoldAndItalicsRegexes = ['__', '_'].map((it) => {
|
||||
return new RegExp(`\\b${it}([^_\\s]|[^_\\s].*?[^_\\s])${it}\\b`, 'g');
|
||||
});
|
||||
|
||||
let result = text;
|
||||
|
||||
underscoreBoldAndItalicsRegexes.forEach((regex) => {
|
||||
result = result.replace(regex, '$1');
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
function genericAnchorGenerator(text: string, concatSpaces: boolean) {
|
||||
let result = text;
|
||||
result = result.toLowerCase();
|
||||
result = transformInlineCode(result, (s: string) => {
|
||||
return stripNonLatinCharacters(s);
|
||||
});
|
||||
result = removeUnderscoreBoldAndItalics(result);
|
||||
result = stripHtmlTags(result);
|
||||
result = stripMarkdownLinks(result);
|
||||
result = result.trim();
|
||||
result = stripNonLatinCharacters(result);
|
||||
result = spacesToDash(result);
|
||||
if (concatSpaces) {
|
||||
result = concatDashes(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function escapeRegExp(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
interface Title {
|
||||
level: number
|
||||
id: string
|
||||
name: string
|
||||
md: string
|
||||
}
|
||||
|
||||
function getTitles(markdown: string, idGenerator: (titleMarkdownContent: string) => string) {
|
||||
const titles: Title[] = [];
|
||||
|
||||
markdown = markdown.replace(/^```[\s\S]*?\n```/mg, () => {
|
||||
return '';
|
||||
});
|
||||
markdown = markdown.replace(/^~~~[\s\S]*?\n~~~/mg, () => {
|
||||
return '';
|
||||
});
|
||||
|
||||
[...markdown.matchAll(/^(#+)(.*$)/mg)].forEach(
|
||||
([match, levelString, titleContent]) => {
|
||||
const level = levelString.length;
|
||||
|
||||
titles.push({
|
||||
md: match,
|
||||
level,
|
||||
id: idGenerator(titleContent),
|
||||
name: titleContent.trim(),
|
||||
});
|
||||
});
|
||||
|
||||
return titles;
|
||||
};
|
||||
|
||||
export function getTocMarkdown({
|
||||
markdown,
|
||||
generateAnchors = true,
|
||||
indentChars = '-*+',
|
||||
indentSpaces = 2,
|
||||
maxLevel = -1,
|
||||
anchorPrefix = '',
|
||||
concatSpaces = true,
|
||||
commentStyle = 'html',
|
||||
}: {
|
||||
markdown: string
|
||||
generateAnchors?: boolean
|
||||
indentChars?: string
|
||||
indentSpaces?: number
|
||||
maxLevel?: number
|
||||
anchorPrefix?: string
|
||||
concatSpaces?: boolean
|
||||
commentStyle?: 'html' | 'liquid'
|
||||
}) {
|
||||
const allIds: { [id: string]: number } = {};
|
||||
const getFinalId = (id: string) => {
|
||||
if (typeof allIds[id] === 'undefined') {
|
||||
allIds[id] = 0;
|
||||
return id;
|
||||
}
|
||||
else {
|
||||
allIds[id] += 1;
|
||||
return `${id}-${allIds[id]}`;
|
||||
}
|
||||
};
|
||||
const titles = getTitles(markdown, titleContent => getFinalId(genericAnchorGenerator(titleContent, concatSpaces)));
|
||||
|
||||
const createLink = (linkText: string, url: string) => {
|
||||
return `[${linkText.replace('[', '\\[').replace(']', '\\]')}](${url.replace('(', '%28').replace(')', '%29')})`;
|
||||
};
|
||||
|
||||
let markdownTOC = '';
|
||||
let resultMarkdown = markdown;
|
||||
const commentOpen = commentStyle === 'html' ? '<!--' : '{%- #';
|
||||
const commentClose = commentStyle === 'html' ? '-->' : '-%}';
|
||||
|
||||
resultMarkdown = resultMarkdown.replace(
|
||||
new RegExp(`\n${commentOpen} TOC START.*?TOC END ${commentClose}\n`, 'smg'),
|
||||
'\n[TOC]\n',
|
||||
);
|
||||
resultMarkdown = resultMarkdown.replace(
|
||||
new RegExp(`^${commentOpen} TOC ANCHOR.*?\n`, 'mg'),
|
||||
'',
|
||||
);
|
||||
|
||||
titles.forEach((title) => {
|
||||
if (title.level === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxLevel > 0 && title.level > maxLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const level = title.level - 2;
|
||||
let offset = '';
|
||||
if (level) {
|
||||
offset = `${Array.from({ length: level * indentSpaces }).join(' ')} `;
|
||||
}
|
||||
const bulletChar = indentChars[level] ?? indentChars.slice(-1)[0];
|
||||
|
||||
const anchorName = `${anchorPrefix}${title.id}`;
|
||||
|
||||
markdownTOC += `${offset}${bulletChar} ${createLink(stripMarkdownLinks(title.name), `#${anchorName}`)}\n`;
|
||||
|
||||
if (generateAnchors) {
|
||||
resultMarkdown = resultMarkdown.replace(
|
||||
new RegExp(`(?<!^${commentOpen} TOC ANCHOR.*\n)^${escapeRegExp(title.md)}`, 'm'),
|
||||
`${commentOpen} TOC ANCHOR ${commentClose}<a name="${title.id}"></a>\n${title.md}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
resultMarkdown = resultMarkdown.replace(
|
||||
/^\[TOC\]\n/mg,
|
||||
`${commentOpen} TOC START ${commentClose}\n${markdownTOC}${commentOpen} TOC END ${commentClose}\n`,
|
||||
);
|
||||
|
||||
return resultMarkdown;
|
||||
}
|
91
src/tools/markdown-toc-generator/markdown-toc-generator.vue
Normal file
91
src/tools/markdown-toc-generator/markdown-toc-generator.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { withDefaultOnError } from '../../utils/defaults';
|
||||
import {
|
||||
getTocMarkdown,
|
||||
} from './markdown-toc-generator.service';
|
||||
import { useQueryParamOrStorage } from '@/composable/queryParams';
|
||||
|
||||
const markdown = ref('');
|
||||
const generateAnchors = useQueryParamOrStorage({ name: 'anchors', storageName: 'md-toc-gen:anchors', defaultValue: true });
|
||||
const indentChars = useQueryParamOrStorage({ name: 'bullets', storageName: 'md-toc-gen:bullets', defaultValue: '-*+' });
|
||||
const indentSpaces = ref(2);
|
||||
const maxLevel = useQueryParamOrStorage({ name: 'max', storageName: 'md-toc-gen:max', defaultValue: -1 });
|
||||
const anchorPrefix = useQueryParamOrStorage({ name: 'prefix', storageName: 'md-toc-gen:prefix', defaultValue: '' });
|
||||
const concatSpaces = useQueryParamOrStorage({ name: 'concat', storageName: 'md-toc-gen:concat', defaultValue: true });
|
||||
const commentStyle = useQueryParamOrStorage({ name: 'comment', storageName: 'md-toc-gen:comment', defaultValue: 'html' });
|
||||
|
||||
const markdownWithTOC = computed(() => withDefaultOnError(() => {
|
||||
return getTocMarkdown({
|
||||
markdown: markdown.value,
|
||||
anchorPrefix: anchorPrefix.value,
|
||||
commentStyle: commentStyle.value as ('html' | 'liquid'),
|
||||
concatSpaces: concatSpaces.value,
|
||||
generateAnchors: generateAnchors.value,
|
||||
indentChars: indentChars.value,
|
||||
indentSpaces: indentSpaces.value,
|
||||
maxLevel: maxLevel.value,
|
||||
});
|
||||
}, ''));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-card title="Options" mb-2>
|
||||
<n-form-item label="Generate Anchors" label-placement="left">
|
||||
<n-checkbox v-model:checked="generateAnchors" mr-2 />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="indentChars"
|
||||
label="Bullet Chars"
|
||||
placeholder="Bullet Chars"
|
||||
mb-2
|
||||
/>
|
||||
|
||||
<n-form-item label="Indents: " label-placement="left">
|
||||
<n-input-number v-model:value="indentSpaces" placeholder="Indents..." :max="10" :min="1" w-full />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Max Heading Level: " label-placement="left">
|
||||
<n-input-number v-model:value="maxLevel" placeholder="Max Heading Level..." :max="6" :min="-1" w-full />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="anchorPrefix"
|
||||
label="Anchors Prefix"
|
||||
placeholder="Anchors Prefix"
|
||||
mb-2
|
||||
/>
|
||||
|
||||
<n-form-item label="Concat Spaces" label-placement="left">
|
||||
<n-checkbox v-model:checked="concatSpaces" mr-2 />
|
||||
</n-form-item>
|
||||
|
||||
<c-select
|
||||
v-model:value="commentStyle"
|
||||
label="Comment Styles"
|
||||
:options="['html', 'liquid']"
|
||||
placeholder="Comment Styles"
|
||||
/>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Input markdown" mb-2>
|
||||
<n-p>You can paste a document with existing TOC (generated by this tool) or add a <code>[TOC]</code> marker in your document (on a single line)</n-p>
|
||||
<c-input-text
|
||||
v-model:value="markdown"
|
||||
placeholder="Put your markdown here..."
|
||||
multline
|
||||
rows="8"
|
||||
mb-2
|
||||
mt-2
|
||||
/>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Output markdown with TOC" mb-2>
|
||||
<textarea-copyable
|
||||
language="markdown"
|
||||
:value="markdownWithTOC"
|
||||
/>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
@ -38,7 +38,8 @@ describe('base64 utils', () => {
|
||||
|
||||
it('should throw for 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');
|
||||
// missing final '='
|
||||
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
|
||||
@ -56,17 +57,17 @@ describe('base64 utils', () => {
|
||||
|
||||
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);
|
||||
it('should return true for untrimmed correct base64 string', () => {
|
||||
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(true);
|
||||
expect(isValidBase64(' LTE=')).to.eql(true);
|
||||
expect(isValidBase64(' YQ== ')).to.eql(true);
|
||||
expect(isValidBase64(' ')).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
|
||||
|
||||
function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
|
||||
const encoded = window.btoa(str);
|
||||
const encoded = Base64.encode(str);
|
||||
return makeUrlSafe ? makeUriSafe(encoded) : encoded;
|
||||
}
|
||||
|
||||
@ -16,7 +18,7 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool
|
||||
}
|
||||
|
||||
try {
|
||||
return window.atob(cleanStr);
|
||||
return Base64.decode(cleanStr);
|
||||
}
|
||||
catch (_) {
|
||||
throw new Error('Incorrect base64 string');
|
||||
@ -34,10 +36,11 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo
|
||||
}
|
||||
|
||||
try {
|
||||
const reEncodedBase64 = Base64.fromUint8Array(Base64.toUint8Array(cleanStr));
|
||||
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) {
|
||||
return false;
|
||||
|
Loading…
Reference in New Issue
Block a user