mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-04 21:37:11 -04:00
Merge branch 'main' into feat/css-xpath-converter
This commit is contained in:
commit
bee22b877a
42 changed files with 1439 additions and 87 deletions
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
||||
import { IconBrandGithub, IconBrandX, IconInfoCircle, IconMoon, IconSun } from '@tabler/icons-vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
|
@ -16,32 +16,32 @@ const { isDarkTheme } = toRefs(styleStore);
|
|||
rel="noopener noreferrer"
|
||||
:aria-label="$t('home.nav.githubRepository')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
<n-icon size="25" :component="IconBrandGithub" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
|
||||
<c-tooltip :tooltip="$t('home.nav.twitterX')" position="bottom">
|
||||
<c-button
|
||||
circle
|
||||
variant="text"
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
href="https://x.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
:aria-label="$t('home.nav.twitterAccount')"
|
||||
:aria-label="$t('home.nav.twitterXAccount')"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
<n-icon size="25" :component="IconBrandX" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
|
||||
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
|
||||
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
<n-icon size="25" :component="IconInfoCircle" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
|
||||
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="IconSun" />
|
||||
<n-icon v-else size="25" :component="IconMoon" />
|
||||
</c-button>
|
||||
</c-tooltip>
|
||||
</template>
|
||||
|
|
|
@ -7,6 +7,7 @@ import sqlHljs from 'highlight.js/lib/languages/sql';
|
|||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||
import iniHljs from 'highlight.js/lib/languages/ini';
|
||||
import markdownHljs from 'highlight.js/lib/languages/markdown';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -30,6 +31,7 @@ hljs.registerLanguage('html', xmlHljs);
|
|||
hljs.registerLanguage('xml', xmlHljs);
|
||||
hljs.registerLanguage('yaml', yamlHljs);
|
||||
hljs.registerLanguage('toml', iniHljs);
|
||||
hljs.registerLanguage('markdown', markdownHljs);
|
||||
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ import _ from 'lodash';
|
|||
import { type Ref, reactive, watch } from 'vue';
|
||||
|
||||
type ValidatorReturnType = unknown;
|
||||
type GetErrorMessageReturnType = string;
|
||||
|
||||
export interface UseValidationRule<T> {
|
||||
validator: (value: T) => ValidatorReturnType
|
||||
getErrorMessage?: (value: T) => GetErrorMessageReturnType
|
||||
message: string
|
||||
}
|
||||
|
||||
|
@ -24,6 +26,15 @@ export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export function getErrorMessageOrThrown(cb: () => GetErrorMessageReturnType): string {
|
||||
try {
|
||||
return cb() || '';
|
||||
}
|
||||
catch (e: any) {
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ValidationAttrs {
|
||||
feedback: string
|
||||
validationStatus: string | undefined
|
||||
|
@ -61,7 +72,13 @@ export function useValidation<T>({
|
|||
|
||||
for (const rule of get(rules)) {
|
||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||
state.message = rule.message;
|
||||
if (rule.getErrorMessage) {
|
||||
const getErrorMessage = rule.getErrorMessage;
|
||||
state.message = rule.message.replace('{0}', getErrorMessageOrThrown(() => getErrorMessage(source.value)));
|
||||
}
|
||||
else {
|
||||
state.message = rule.message;
|
||||
}
|
||||
state.status = 'error';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,12 @@ export const config = figue({
|
|||
default: false,
|
||||
env: 'VITE_SHOW_BANNER',
|
||||
},
|
||||
showSponsorBanner: {
|
||||
doc: 'Show the sponsor banner',
|
||||
format: 'boolean',
|
||||
default: false,
|
||||
env: 'VITE_SHOW_SPONSOR_BANNER',
|
||||
},
|
||||
})
|
||||
.loadEnv({
|
||||
...import.meta.env,
|
||||
|
|
|
@ -81,7 +81,7 @@ const tools = computed<ToolCategory[]>(() => [
|
|||
</div>
|
||||
<div>
|
||||
© {{ new Date().getFullYear() }}
|
||||
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh">
|
||||
<c-link target="_blank" rel="noopener" href="https://corentin.tech?utm_source=it-tools&utm_medium=footer">
|
||||
Corentin Thomasset
|
||||
</c-link>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@ const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.descrip
|
|||
</n-h1>
|
||||
|
||||
<div>
|
||||
<FavoriteButton :tool="{ name: route.meta.name } as Tool" />
|
||||
<FavoriteButton :tool="{ name: route.meta.name, path: route.path } as Tool" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createPinia } from 'pinia';
|
|||
import { createHead } from '@vueuse/head';
|
||||
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
import shadow from 'vue-shadow-dom';
|
||||
import { plausible } from './plugins/plausible.plugin';
|
||||
|
||||
import 'virtual:uno.css';
|
||||
|
@ -23,5 +24,6 @@ app.use(i18nPlugin);
|
|||
app.use(router);
|
||||
app.use(naive);
|
||||
app.use(plausible);
|
||||
app.use(shadow);
|
||||
|
||||
app.mount('#app');
|
||||
|
|
|
@ -6,6 +6,7 @@ const localesLong: Record<string, string> = {
|
|||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
no: 'Norwegian',
|
||||
pt: 'Português',
|
||||
ru: 'Русский',
|
||||
uk: 'Українська',
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { Heart } from '@vicons/tabler';
|
||||
import { IconDragDrop, IconHeart } from '@tabler/icons-vue';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import { computed } from 'vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import ColoredCard from '../components/ColoredCard.vue';
|
||||
import ToolCard from '../components/ToolCard.vue';
|
||||
import { useToolStore } from '@/tools/tools.store';
|
||||
|
@ -10,13 +12,20 @@ const toolStore = useToolStore();
|
|||
|
||||
useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||
const { t } = useI18n();
|
||||
|
||||
const favoriteTools = computed(() => toolStore.favoriteTools);
|
||||
|
||||
// Update favorite tools order when drag is finished
|
||||
function onUpdateFavoriteTools() {
|
||||
toolStore.updateFavoriteTools(favoriteTools.value); // Update the store with the new order
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pt-50px">
|
||||
<div class="grid-wrapper">
|
||||
<div v-if="config.showBanner" class="grid grid-cols-1 gap-12px lg:grid-cols-3 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
|
||||
<div class="grid grid-cols-1 gap-12px lg:grid-cols-3 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<ColoredCard v-if="config.showBanner" :title="$t('home.follow.title')" :icon="IconHeart">
|
||||
{{ $t('home.follow.p1') }}
|
||||
<a
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
|
@ -26,24 +35,61 @@ const { t } = useI18n();
|
|||
>GitHub</a>
|
||||
{{ $t('home.follow.p2') }}
|
||||
<a
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
href="https://x.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
:aria-label="$t('home.follow.twitterAccount')"
|
||||
>Twitter</a>.
|
||||
:aria-label="$t('home.follow.twitterXAccount')"
|
||||
>X</a>.
|
||||
{{ $t('home.follow.thankYou') }}
|
||||
<n-icon :component="Heart" />
|
||||
<n-icon :component="IconHeart" />
|
||||
</ColoredCard>
|
||||
|
||||
<a href="https://bit.ly/3zBl7DG" target="_blank" rel="noopener" class="text-current decoration-none">
|
||||
<c-card v-if="config.showSponsorBanner" class="cursor-pointer !border-2px !hover:border-primary">
|
||||
<div class="dark:hidden">
|
||||
<svg width="40" height="40" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M458.592 250.639C425.688 222.824 376.118 211.673 332.186 244.145C330.164 245.615 327.651 243.103 329.183 241.143C339.6 227.725 351.67 213.266 361.413 198.746C371.339 183.858 386.167 173.198 403.262 167.99C494.253 140.42 466.925 6 466.925 6C466.925 6 326.365 15.0675 343.705 136.315C346.585 156.594 341.193 177.241 328.509 193.354C312.946 213.021 294.87 231.83 281.758 245.431C279.001 248.25 274.344 245.554 275.447 241.755C288.13 199.052 297.383 133.007 253.45 90.4259L191.625 39.0842L179.738 54.7685C144.384 101.393 154.739 167.132 201.429 202.422C228.205 222.64 240.337 244.635 238.438 268.774C237.274 283.233 230.718 296.773 220.914 307.495C202.471 327.713 185.253 349.402 171.957 374.521C170.119 378.013 164.788 376.665 164.972 372.683C166.871 331.205 162.888 237.712 93.037 204.321L14.8527 174.117L8.78665 192.19C-10.882 250.517 21.2863 312.825 79.557 332.614C130.23 349.83 148.305 382.486 136.112 431.438C135.561 433.215 126.737 483.638 127.963 506H184.15C186.049 471.323 222.446 448.532 254.001 462.684C262.886 466.667 272.016 472.364 281.39 479.717C331.634 519.295 405.652 509.921 445.173 459.621L456.447 445.284L385.371 394.249C336.597 355.896 271.525 373.234 223.365 406.074C219.321 408.831 214.174 404.419 216.441 400.008C274.65 285.806 350.322 286.051 379.979 311.416C415.946 342.172 470.418 336.658 500.932 300.572L509.694 290.218L458.531 250.639H458.592Z" fill="#aaaaaa" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="hidden dark:block">
|
||||
<svg width="40" height="40" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M458.592 250.639C425.688 222.824 376.118 211.673 332.186 244.145C330.164 245.615 327.651 243.103 329.183 241.143C339.6 227.725 351.67 213.266 361.413 198.746C371.339 183.858 386.167 173.198 403.262 167.99C494.253 140.42 466.925 6 466.925 6C466.925 6 326.365 15.0675 343.705 136.315C346.585 156.594 341.193 177.241 328.509 193.354C312.946 213.021 294.87 231.83 281.758 245.431C279.001 248.25 274.344 245.554 275.447 241.755C288.13 199.052 297.383 133.007 253.45 90.4259L191.625 39.0842L179.738 54.7685C144.384 101.393 154.739 167.132 201.429 202.422C228.205 222.64 240.337 244.635 238.438 268.774C237.274 283.233 230.718 296.773 220.914 307.495C202.471 327.713 185.253 349.402 171.957 374.521C170.119 378.013 164.788 376.665 164.972 372.683C166.871 331.205 162.888 237.712 93.037 204.321L14.8527 174.117L8.78665 192.19C-10.882 250.517 21.2863 312.825 79.557 332.614C130.23 349.83 148.305 382.486 136.112 431.438C135.561 433.215 126.737 483.638 127.963 506H184.15C186.049 471.323 222.446 448.532 254.001 462.684C262.886 466.667 272.016 472.364 281.39 479.717C331.634 519.295 405.652 509.921 445.173 459.621L456.447 445.284L385.371 394.249C336.597 355.896 271.525 373.234 223.365 406.074C219.321 408.831 214.174 404.419 216.441 400.008C274.65 285.806 350.322 286.051 379.979 311.416C415.946 342.172 470.418 336.658 500.932 300.572L509.694 290.218L458.531 250.639H458.592Z" fill="#505050" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="my-5px flex items-baseline gap-4 text-lg text-black dark:text-white">
|
||||
Fern <div class="rounded-full bg-#eeeeee px-10px py-2px text-xs text-black dark:bg-#333333 dark:text-white">
|
||||
Sponsor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-neutral-500 dark:text-neutral-400">
|
||||
Offer developer documentation that looks as good as Stripe's using Fern. <a href="https://bit.ly/3zBl7DG" target="_blank" rel="noopener" class="font-bold text-current transition hover:text-primary">Request a preview</a> of your docs on Fern.
|
||||
</div>
|
||||
</c-card>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<transition name="height">
|
||||
<div v-if="toolStore.favoriteTools.length > 0">
|
||||
<h3 class="mb-5px mt-25px font-500 text-neutral-400">
|
||||
{{ $t('home.categories.favoriteTools') }}
|
||||
<c-tooltip :tooltip="$t('home.categories.favoritesDndToolTip')">
|
||||
<n-icon :component="IconDragDrop" size="18" />
|
||||
</c-tooltip>
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-12px lg:grid-cols-3 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<ToolCard v-for="tool in toolStore.favoriteTools" :key="tool.name" :tool="tool" />
|
||||
</div>
|
||||
<Draggable
|
||||
:list="favoriteTools"
|
||||
class="grid grid-cols-1 gap-12px lg:grid-cols-3 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-4"
|
||||
ghost-class="ghost-favorites-draggable"
|
||||
item-key="name"
|
||||
@end="onUpdateFavoriteTools"
|
||||
>
|
||||
<template #item="{ element: tool }">
|
||||
<ToolCard :tool="tool" />
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
|
@ -81,4 +127,24 @@ const { t } = useI18n();
|
|||
opacity: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ghost-favorites-draggable {
|
||||
opacity: 0.4;
|
||||
background-color: #ccc;
|
||||
border: 2px dashed #666;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.1);
|
||||
animation: ghost-favorites-draggable-animation 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes ghost-favorites-draggable-animation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -84,8 +84,8 @@ const items: MenuItem[] = [
|
|||
type: 'button',
|
||||
icon: H3,
|
||||
title: 'Heading 3',
|
||||
action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
|
||||
isActive: () => editor.value.isActive('heading', { level: 4 }),
|
||||
action: () => editor.value.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
isActive: () => editor.value.isActive('heading', { level: 3 }),
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
|
|
|
@ -10,6 +10,9 @@ import { tool as textToUnicode } from './text-to-unicode';
|
|||
import { tool as safelinkDecoder } from './safelink-decoder';
|
||||
import { tool as xmlToJson } from './xml-to-json';
|
||||
import { tool as jsonToXml } from './json-to-xml';
|
||||
import { tool as regexTester } from './regex-tester';
|
||||
import { tool as regexMemo } from './regex-memo';
|
||||
import { tool as markdownToHtml } from './markdown-to-html';
|
||||
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
|
@ -113,6 +116,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
tomlToYaml,
|
||||
xmlToJson,
|
||||
jsonToXml,
|
||||
markdownToHtml,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -158,6 +162,8 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
cssXpathConverter,
|
||||
cssSelectorsMemo,
|
||||
xpathMemo,
|
||||
regexTester,
|
||||
regexMemo,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
12
src/tools/markdown-to-html/index.ts
Normal file
12
src/tools/markdown-to-html/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Markdown } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Markdown to HTML',
|
||||
path: '/markdown-to-html',
|
||||
description: 'Convert Markdown to Html and allow to print (as PDF)',
|
||||
keywords: ['markdown', 'html', 'converter', 'pdf'],
|
||||
component: () => import('./markdown-to-html.vue'),
|
||||
icon: Markdown,
|
||||
createdAt: new Date('2024-08-25'),
|
||||
});
|
44
src/tools/markdown-to-html/markdown-to-html.vue
Normal file
44
src/tools/markdown-to-html/markdown-to-html.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import markdownit from 'markdown-it';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
const inputMarkdown = ref('');
|
||||
const outputHtml = computed(() => {
|
||||
const md = markdownit();
|
||||
return md.render(inputMarkdown.value);
|
||||
});
|
||||
|
||||
function printHtml() {
|
||||
const w = window.open();
|
||||
if (w === null) {
|
||||
return;
|
||||
}
|
||||
w.document.body.innerHTML = outputHtml.value;
|
||||
w.print();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text
|
||||
v-model:value="inputMarkdown"
|
||||
multiline raw-text
|
||||
placeholder="Your Markdown content..."
|
||||
rows="8"
|
||||
autofocus
|
||||
label="Your Markdown to convert:"
|
||||
/>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-form-item label="Output HTML:">
|
||||
<TextareaCopyable :value="outputHtml" :word-wrap="true" language="html" />
|
||||
</n-form-item>
|
||||
|
||||
<div flex justify-center>
|
||||
<n-button @click="printHtml">
|
||||
Print as PDF
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
12
src/tools/regex-memo/index.ts
Normal file
12
src/tools/regex-memo/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { BrandJavascript } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Regex cheatsheet',
|
||||
path: '/regex-memo',
|
||||
description: 'Javascript Regex/Regular Expression cheatsheet',
|
||||
keywords: ['regex', 'regular', 'expression', 'javascript', 'memo', 'cheatsheet'],
|
||||
component: () => import('./regex-memo.vue'),
|
||||
icon: BrandJavascript,
|
||||
createdAt: new Date('2024-09-20'),
|
||||
});
|
121
src/tools/regex-memo/regex-memo.content.md
Normal file
121
src/tools/regex-memo/regex-memo.content.md
Normal file
|
@ -0,0 +1,121 @@
|
|||
### Normal characters
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`.` or `[^\n\r]` | any character *excluding* a newline or carriage return
|
||||
`[A-Za-z]` | alphabet
|
||||
`[a-z]` | lowercase alphabet
|
||||
`[A-Z]` | uppercase alphabet
|
||||
`\d` or `[0-9]` | digit
|
||||
`\D` or `[^0-9]` | non-digit
|
||||
`_` | underscore
|
||||
`\w` or `[A-Za-z0-9_]` | alphabet, digit or underscore
|
||||
`\W` or `[^A-Za-z0-9_]` | inverse of `\w`
|
||||
`\S` | inverse of `\s`
|
||||
|
||||
### Whitespace characters
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
` ` | space
|
||||
`\t` | tab
|
||||
`\n` | newline
|
||||
`\r` | carriage return
|
||||
`\s` | space, tab, newline or carriage return
|
||||
|
||||
### Character set
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`[xyz]` | either `x`, `y` or `z`
|
||||
`[^xyz]` | neither `x`, `y` nor `z`
|
||||
`[1-3]` | either `1`, `2` or `3`
|
||||
`[^1-3]` | neither `1`, `2` nor `3`
|
||||
|
||||
- Think of a character set as an `OR` operation on the single characters that are enclosed between the square brackets.
|
||||
- Use `^` after the opening `[` to “negate” the character set.
|
||||
- Within a character set, `.` means a literal period.
|
||||
|
||||
### Characters that require escaping
|
||||
|
||||
#### Outside a character set
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`\.` | period
|
||||
`\^` | caret
|
||||
`\$` | dollar sign
|
||||
`\|` | pipe
|
||||
`\\` | back slash
|
||||
`\/` | forward slash
|
||||
`\(` | opening bracket
|
||||
`\)` | closing bracket
|
||||
`\[` | opening square bracket
|
||||
`\]` | closing square bracket
|
||||
`\{` | opening curly bracket
|
||||
`\}` | closing curly bracket
|
||||
|
||||
#### Inside a character set
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`\\` | back slash
|
||||
`\]` | closing square bracket
|
||||
|
||||
- A `^` must be escaped only if it occurs immediately after the opening `[` of the character set.
|
||||
- A `-` must be escaped only if it occurs between two alphabets or two digits.
|
||||
|
||||
### Quantifiers
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`{2}` | exactly 2
|
||||
`{2,}` | at least 2
|
||||
`{2,7}` | at least 2 but no more than 7
|
||||
`*` | 0 or more
|
||||
`+` | 1 or more
|
||||
`?` | exactly 0 or 1
|
||||
|
||||
- The quantifier goes *after* the expression to be quantified.
|
||||
|
||||
### Boundaries
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`^` | start of string
|
||||
`$` | end of string
|
||||
`\b` | word boundary
|
||||
|
||||
- How word boundary matching works:
|
||||
- At the beginning of the string if the first character is `\w`.
|
||||
- Between two adjacent characters within the string, if the first character is `\w` and the second character is `\W`.
|
||||
- At the end of the string if the last character is `\w`.
|
||||
|
||||
### Matching
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`foo\|bar` | match either `foo` or `bar`
|
||||
`foo(?=bar)` | match `foo` if it’s before `bar`
|
||||
`foo(?!bar)` | match `foo` if it’s *not* before `bar`
|
||||
`(?<=bar)foo` | match `foo` if it’s after `bar`
|
||||
`(?<!bar)foo` | match `foo` if it’s *not* after `bar`
|
||||
|
||||
### Grouping and capturing
|
||||
|
||||
Expression | Description
|
||||
:--|:--
|
||||
`(foo)` | capturing group; match and capture `foo`
|
||||
`(?:foo)` | non-capturing group; match `foo` but *without* capturing `foo`
|
||||
`(foo)bar\1` | `\1` is a backreference to the 1st capturing group; match `foobarfoo`
|
||||
|
||||
- Capturing groups are only relevant in the following methods:
|
||||
- `string.match(regexp)`
|
||||
- `string.matchAll(regexp)`
|
||||
- `string.replace(regexp, callback)`
|
||||
- `\N` is a backreference to the `Nth` capturing group. Capturing groups are numbered starting from 1.
|
||||
|
||||
## References and tools
|
||||
|
||||
- [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
|
||||
- [RegExplained](https://leaverou.github.io/regexplained/)
|
32
src/tools/regex-memo/regex-memo.vue
Normal file
32
src/tools/regex-memo/regex-memo.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import Memo from './regex-memo.content.md';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Memo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(pre) {
|
||||
margin: 0;
|
||||
padding: 15px 22px;
|
||||
background-color: v-bind('themeVars.cardColor');
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
::v-deep(table) {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
::v-deep(table), ::v-deep(td), ::v-deep(th) {
|
||||
border: 1px solid v-bind('themeVars.textColor1');
|
||||
padding: 5px;
|
||||
}
|
||||
::v-deep(a) {
|
||||
color: v-bind('themeVars.textColor1');
|
||||
}
|
||||
</style>
|
12
src/tools/regex-tester/index.ts
Normal file
12
src/tools/regex-tester/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Language } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Regex Tester',
|
||||
path: '/regex-tester',
|
||||
description: 'Test your regular expressions with sample text.',
|
||||
keywords: ['regex', 'tester', 'sample', 'expression'],
|
||||
component: () => import('./regex-tester.vue'),
|
||||
icon: Language,
|
||||
createdAt: new Date('2024-09-20'),
|
||||
});
|
106
src/tools/regex-tester/regex-tester.service.test.ts
Normal file
106
src/tools/regex-tester/regex-tester.service.test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { matchRegex } from './regex-tester.service';
|
||||
|
||||
const regexesData = [
|
||||
{
|
||||
regex: '',
|
||||
text: '',
|
||||
flags: '',
|
||||
result: [],
|
||||
},
|
||||
{
|
||||
regex: '.*',
|
||||
text: '',
|
||||
flags: '',
|
||||
result: [],
|
||||
},
|
||||
{
|
||||
regex: '',
|
||||
text: 'aaa',
|
||||
flags: '',
|
||||
result: [],
|
||||
},
|
||||
{
|
||||
regex: 'a',
|
||||
text: 'baaa',
|
||||
flags: '',
|
||||
result: [
|
||||
{
|
||||
captures: [],
|
||||
groups: [],
|
||||
index: 1,
|
||||
value: 'a',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
regex: '(.)(?<g>r)',
|
||||
text: 'azertyr',
|
||||
flags: 'g',
|
||||
result: [
|
||||
{
|
||||
captures: [
|
||||
{
|
||||
end: 3,
|
||||
name: '1',
|
||||
start: 2,
|
||||
value: 'e',
|
||||
},
|
||||
{
|
||||
end: 4,
|
||||
name: '2',
|
||||
start: 3,
|
||||
value: 'r',
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
end: 4,
|
||||
name: 'g',
|
||||
start: 3,
|
||||
value: 'r',
|
||||
},
|
||||
],
|
||||
index: 2,
|
||||
value: 'er',
|
||||
},
|
||||
{
|
||||
captures: [
|
||||
{
|
||||
end: 6,
|
||||
name: '1',
|
||||
start: 5,
|
||||
value: 'y',
|
||||
},
|
||||
{
|
||||
end: 7,
|
||||
name: '2',
|
||||
start: 6,
|
||||
value: 'r',
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
end: 7,
|
||||
name: 'g',
|
||||
start: 6,
|
||||
value: 'r',
|
||||
},
|
||||
],
|
||||
index: 5,
|
||||
value: 'yr',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('regex-tester', () => {
|
||||
for (const reg of regexesData) {
|
||||
const { regex, text, flags, result: expected_result } = reg;
|
||||
it(`Should matchRegex("${regex}","${text}","${flags}") return correct result`, async () => {
|
||||
const result = matchRegex(regex, text, `${flags}d`);
|
||||
|
||||
expect(result).to.deep.equal(expected_result);
|
||||
});
|
||||
}
|
||||
});
|
61
src/tools/regex-tester/regex-tester.service.ts
Normal file
61
src/tools/regex-tester/regex-tester.service.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
interface RegExpGroupIndices {
|
||||
[name: string]: [number, number]
|
||||
}
|
||||
interface RegExpIndices extends Array<[number, number]> {
|
||||
groups: RegExpGroupIndices
|
||||
}
|
||||
interface RegExpExecArrayWithIndices extends RegExpExecArray {
|
||||
indices: RegExpIndices
|
||||
}
|
||||
interface GroupCapture {
|
||||
name: string
|
||||
value: string
|
||||
start: number
|
||||
end: number
|
||||
};
|
||||
|
||||
export function matchRegex(regex: string, text: string, flags: string) {
|
||||
// if (regex === '' || text === '') {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
let lastIndex = -1;
|
||||
const re = new RegExp(regex, flags);
|
||||
const results = [];
|
||||
let match = re.exec(text) as RegExpExecArrayWithIndices;
|
||||
while (match !== null) {
|
||||
if (re.lastIndex === lastIndex || match[0] === '') {
|
||||
break;
|
||||
}
|
||||
const indices = match.indices;
|
||||
const captures: Array<GroupCapture> = [];
|
||||
Object.entries(match).forEach(([captureName, captureValue]) => {
|
||||
if (captureName !== '0' && captureName.match(/\d+/)) {
|
||||
captures.push({
|
||||
name: captureName,
|
||||
value: captureValue,
|
||||
start: indices[Number(captureName)][0],
|
||||
end: indices[Number(captureName)][1],
|
||||
});
|
||||
}
|
||||
});
|
||||
const groups: Array<GroupCapture> = [];
|
||||
Object.entries(match.groups || {}).forEach(([groupName, groupValue]) => {
|
||||
groups.push({
|
||||
name: groupName,
|
||||
value: groupValue,
|
||||
start: indices.groups[groupName][0],
|
||||
end: indices.groups[groupName][1],
|
||||
});
|
||||
});
|
||||
results.push({
|
||||
index: match.index,
|
||||
value: match[0],
|
||||
captures,
|
||||
groups,
|
||||
});
|
||||
lastIndex = re.lastIndex;
|
||||
match = re.exec(text) as RegExpExecArrayWithIndices;
|
||||
}
|
||||
return results;
|
||||
}
|
193
src/tools/regex-tester/regex-tester.vue
Normal file
193
src/tools/regex-tester/regex-tester.vue
Normal file
|
@ -0,0 +1,193 @@
|
|||
<script setup lang="ts">
|
||||
import RandExp from 'randexp';
|
||||
import { render } from '@regexper/render';
|
||||
import type { ShadowRootExpose } from 'vue-shadow-dom';
|
||||
import { matchRegex } from './regex-tester.service';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { useQueryParamOrStorage } from '@/composable/queryParams';
|
||||
|
||||
const regex = useQueryParamOrStorage({ name: 'regex', storageName: 'regex-tester:regex', defaultValue: '' });
|
||||
const text = ref('');
|
||||
const global = ref(true);
|
||||
const ignoreCase = ref(false);
|
||||
const multiline = ref(false);
|
||||
const dotAll = ref(true);
|
||||
const unicode = ref(true);
|
||||
const unicodeSets = ref(false);
|
||||
const visualizerSVG = ref<ShadowRootExpose>();
|
||||
|
||||
const regexValidation = useValidation({
|
||||
source: regex,
|
||||
rules: [
|
||||
{
|
||||
message: 'Invalid regex: {0}',
|
||||
validator: value => new RegExp(value),
|
||||
getErrorMessage: (value) => {
|
||||
const _ = new RegExp(value);
|
||||
return '';
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const results = computed(() => {
|
||||
let flags = 'd';
|
||||
if (global.value) {
|
||||
flags += 'g';
|
||||
}
|
||||
if (ignoreCase.value) {
|
||||
flags += 'i';
|
||||
}
|
||||
if (multiline.value) {
|
||||
flags += 'm';
|
||||
}
|
||||
if (dotAll.value) {
|
||||
flags += 's';
|
||||
}
|
||||
if (unicode.value) {
|
||||
flags += 'u';
|
||||
}
|
||||
else if (unicodeSets.value) {
|
||||
flags += 'v';
|
||||
}
|
||||
|
||||
try {
|
||||
return matchRegex(regex.value, text.value, flags);
|
||||
}
|
||||
catch (_) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const sample = computed(() => {
|
||||
try {
|
||||
const randexp = new RandExp(new RegExp(regex.value.replace(/\(\?\<[^\>]*\>/g, '(?:')));
|
||||
return randexp.gen();
|
||||
}
|
||||
catch (_) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(
|
||||
async () => {
|
||||
const regexValue = regex.value;
|
||||
// shadow root is required:
|
||||
// @regexper/render append a <defs><style> that broke svg transparency of icons in the whole site
|
||||
const visualizer = visualizerSVG.value?.shadow_root;
|
||||
if (visualizer) {
|
||||
while (visualizer.lastChild) {
|
||||
visualizer.removeChild(visualizer.lastChild);
|
||||
}
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
try {
|
||||
await render(regexValue, svg);
|
||||
}
|
||||
catch (_) {
|
||||
}
|
||||
visualizer.appendChild(svg);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div max-w-600px>
|
||||
<c-card title="Regex" mb-1>
|
||||
<c-input-text
|
||||
v-model:value="regex"
|
||||
label="Regex to test:"
|
||||
placeholder="Put the regex to test"
|
||||
multiline
|
||||
rows="3"
|
||||
:validation="regexValidation"
|
||||
/>
|
||||
<router-link target="_blank" to="/regex-memo" mb-1 mt-1>
|
||||
See Regular Expression Cheatsheet
|
||||
</router-link>
|
||||
<n-space>
|
||||
<n-checkbox v-model:checked="global">
|
||||
<span title="Global search">Global search. (<code>g</code>)</span>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="ignoreCase">
|
||||
<span title="Case-insensitive search">Case-insensitive search. (<code>i</code>)</span>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="multiline">
|
||||
<span title="Allows ^ and $ to match next to newline characters.">Multiline(<code>m</code>)</span>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="dotAll">
|
||||
<span title="Allows . to match newline characters.">Singleline(<code>s</code>)</span>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="unicode">
|
||||
<span title="Unicode; treat a pattern as a sequence of Unicode code points.">Unicode(<code>u</code>)</span>
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="unicodeSets">
|
||||
<span title="An upgrade to the u mode with more Unicode features.">Unicode Sets (<code>v</code>)</span>
|
||||
</n-checkbox>
|
||||
</n-space>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<c-input-text
|
||||
v-model:value="text"
|
||||
label="Text to match:"
|
||||
placeholder="Put the text to match"
|
||||
multiline
|
||||
rows="5"
|
||||
/>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Matches" mb-1 mt-3>
|
||||
<n-table v-if="results?.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Index in text
|
||||
</th>
|
||||
<th scope="col">
|
||||
Value
|
||||
</th>
|
||||
<th scope="col">
|
||||
Captures
|
||||
</th>
|
||||
<th scope="col">
|
||||
Groups
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="match of results" :key="match.index">
|
||||
<td>{{ match.index }}</td>
|
||||
<td>{{ match.value }}</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li v-for="capture in match.captures" :key="capture.name">
|
||||
"{{ capture.name }}" = {{ capture.value }} [{{ capture.start }} - {{ capture.end }}]
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li v-for="group in match.groups" :key="group.name">
|
||||
"{{ group.name }}" = {{ group.value }} [{{ group.start }} - {{ group.end }}]
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
<c-alert v-else>
|
||||
No match
|
||||
</c-alert>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Sample matching text" mt-3>
|
||||
<pre style="white-space: pre-wrap; word-break: break-all;">{{ sample }}</pre>
|
||||
</c-card>
|
||||
|
||||
<c-card title="Regex Diagram" style="overflow-x: scroll;" mt-3>
|
||||
<shadow-root ref="visualizerSVG">
|
||||
 
|
||||
</shadow-root>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
|
@ -14,6 +14,7 @@ export const useToolStore = defineStore('tools', () => {
|
|||
|
||||
return ({
|
||||
...tool,
|
||||
path: tool.path,
|
||||
name: t(`tools.${toolI18nKey}.title`, tool.name),
|
||||
description: t(`tools.${toolI18nKey}.description`, tool.description),
|
||||
category: t(`tools.categories.${tool.category.toLowerCase()}`, tool.category),
|
||||
|
@ -23,8 +24,9 @@ export const useToolStore = defineStore('tools', () => {
|
|||
const toolsByCategory = computed<ToolCategory[]>(() => {
|
||||
return _.chain(tools.value)
|
||||
.groupBy('category')
|
||||
.map((components, name) => ({
|
||||
.map((components, name, path) => ({
|
||||
name,
|
||||
path,
|
||||
components,
|
||||
}))
|
||||
.value();
|
||||
|
@ -32,7 +34,7 @@ export const useToolStore = defineStore('tools', () => {
|
|||
|
||||
const favoriteTools = computed(() => {
|
||||
return favoriteToolsName.value
|
||||
.map(favoriteName => tools.value.find(({ name }) => name === favoriteName))
|
||||
.map(favoriteName => tools.value.find(({ name, path }) => name === favoriteName || path === favoriteName))
|
||||
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
|
||||
});
|
||||
|
||||
|
@ -43,15 +45,23 @@ export const useToolStore = defineStore('tools', () => {
|
|||
newTools: computed(() => tools.value.filter(({ isNew }) => isNew)),
|
||||
|
||||
addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
favoriteToolsName.value.push(get(tool).name);
|
||||
const toolPath = get(tool).path;
|
||||
if (toolPath) {
|
||||
favoriteToolsName.value.push(toolPath);
|
||||
}
|
||||
},
|
||||
|
||||
removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name);
|
||||
favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name && get(tool).path !== name);
|
||||
},
|
||||
|
||||
isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
return favoriteToolsName.value.includes(get(tool).name);
|
||||
return favoriteToolsName.value.includes(get(tool).name)
|
||||
|| favoriteToolsName.value.includes(get(tool).path);
|
||||
},
|
||||
|
||||
updateFavoriteTools(newOrder: ToolWithCategory[]) {
|
||||
favoriteToolsName.value = newOrder.map(tool => tool.path);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -28,4 +28,53 @@ test.describe('Tool - Yaml to json', () => {
|
|||
`.trim(),
|
||||
);
|
||||
});
|
||||
|
||||
test('Yaml is parsed with merge key and output correct json', async ({ page }) => {
|
||||
await page.getByTestId('input').fill(`
|
||||
default: &default
|
||||
name: ''
|
||||
age: 0
|
||||
|
||||
person:
|
||||
*default
|
||||
|
||||
persons:
|
||||
- <<: *default
|
||||
age: 1
|
||||
- <<: *default
|
||||
name: John
|
||||
- { age: 3, <<: *default }
|
||||
|
||||
`);
|
||||
|
||||
const generatedJson = await page.getByTestId('area-content').innerText();
|
||||
|
||||
expect(generatedJson.trim()).toEqual(
|
||||
`
|
||||
{
|
||||
"default": {
|
||||
"name": "",
|
||||
"age": 0
|
||||
},
|
||||
"person": {
|
||||
"name": "",
|
||||
"age": 0
|
||||
},
|
||||
"persons": [
|
||||
{
|
||||
"name": "",
|
||||
"age": 1
|
||||
},
|
||||
{
|
||||
"name": "John",
|
||||
"age": 0
|
||||
},
|
||||
{
|
||||
"age": 3,
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}`.trim(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import { withDefaultOnError } from '@/utils/defaults';
|
|||
|
||||
function transformer(value: string) {
|
||||
return withDefaultOnError(() => {
|
||||
const obj = parseYaml(value);
|
||||
const obj = parseYaml(value, { merge: true });
|
||||
return obj ? JSON.stringify(obj, null, 3) : '';
|
||||
}, '');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue