Skip to content

Commit 528e88c

Browse files
committed
Merge branch 'feat/code-highlighter' into chore/all-my-stuffs
# Conflicts: # src/tools/index.ts
2 parents 765dcb9 + ebc7956 commit 528e88c

File tree

8 files changed

+199
-4
lines changed

8 files changed

+199
-4
lines changed

.eslintrc-auto-import.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@
286286
"watchTriggerable": true,
287287
"watchWithFilter": true,
288288
"whenever": true,
289-
"toValue": true
289+
"toValue": true,
290+
"injectLocal": true,
291+
"provideLocal": true,
292+
"useClipboardItems": true
290293
}
291294
}

auto-imports.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ declare global {
3636
const h: typeof import('vue')['h']
3737
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
3838
const inject: typeof import('vue')['inject']
39+
const injectLocal: typeof import('@vueuse/core')['injectLocal']
3940
const isDefined: typeof import('@vueuse/core')['isDefined']
4041
const isProxy: typeof import('vue')['isProxy']
4142
const isReactive: typeof import('vue')['isReactive']
@@ -65,6 +66,7 @@ declare global {
6566
const onUpdated: typeof import('vue')['onUpdated']
6667
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
6768
const provide: typeof import('vue')['provide']
69+
const provideLocal: typeof import('@vueuse/core')['provideLocal']
6870
const reactify: typeof import('@vueuse/core')['reactify']
6971
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
7072
const reactive: typeof import('vue')['reactive']
@@ -128,6 +130,7 @@ declare global {
128130
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
129131
const useCached: typeof import('@vueuse/core')['useCached']
130132
const useClipboard: typeof import('@vueuse/core')['useClipboard']
133+
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
131134
const useCloned: typeof import('@vueuse/core')['useCloned']
132135
const useColorMode: typeof import('@vueuse/core')['useColorMode']
133136
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
@@ -326,6 +329,7 @@ declare module 'vue' {
326329
readonly h: UnwrapRef<typeof import('vue')['h']>
327330
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
328331
readonly inject: UnwrapRef<typeof import('vue')['inject']>
332+
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
329333
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
330334
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
331335
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -355,6 +359,7 @@ declare module 'vue' {
355359
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
356360
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
357361
readonly provide: UnwrapRef<typeof import('vue')['provide']>
362+
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
358363
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
359364
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
360365
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -418,6 +423,7 @@ declare module 'vue' {
418423
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
419424
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
420425
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
426+
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
421427
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
422428
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
423429
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
@@ -610,6 +616,7 @@ declare module '@vue/runtime-core' {
610616
readonly h: UnwrapRef<typeof import('vue')['h']>
611617
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
612618
readonly inject: UnwrapRef<typeof import('vue')['inject']>
619+
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
613620
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
614621
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
615622
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -639,6 +646,7 @@ declare module '@vue/runtime-core' {
639646
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
640647
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
641648
readonly provide: UnwrapRef<typeof import('vue')['provide']>
649+
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
642650
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
643651
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
644652
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -702,6 +710,7 @@ declare module '@vue/runtime-core' {
702710
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
703711
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
704712
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
713+
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
705714
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
706715
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
707716
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@types/markdown-it": "^13.0.7",
5151
"@vicons/material": "^0.12.0",
5252
"@vicons/tabler": "^0.12.0",
53-
"@vueuse/core": "^10.3.0",
53+
"@vueuse/core": "^10.11.1",
5454
"@vueuse/head": "^1.0.0",
5555
"@vueuse/router": "^10.0.0",
5656
"@zxing/library": "^0.21.0",
@@ -75,7 +75,7 @@
7575
"iarna-toml-esm": "^3.0.5",
7676
"ibantools": "^4.3.3",
7777
"js-base64": "^3.7.7",
78-
"jsbarcode": "^3.11.7",
78+
"jsbarcode": "^3.11.6",
7979
"json5": "^2.2.3",
8080
"jwt-decode": "^3.1.2",
8181
"libphonenumber-js": "^1.10.28",
@@ -95,6 +95,8 @@
9595
"plausible-tracker": "^0.3.8",
9696
"qrcode": "^1.5.1",
9797
"randexp": "^0.5.3",
98+
"regex": "^4.3.3",
99+
"shiki": "^1.22.0",
98100
"sql-formatter": "^13.0.0",
99101
"sshpk": "^1.18.0",
100102
"ua-parser-js": "^1.0.35",

src/composable/copy.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line no-restricted-imports
2-
import { useClipboard } from '@vueuse/core';
2+
import { useClipboard, useClipboardItems } from '@vueuse/core';
33
import { useMessage } from 'naive-ui';
44
import type { MaybeRefOrGetter } from 'vue';
55

@@ -28,3 +28,34 @@ export function useCopy({ source, text = 'Copied to the clipboard', createToast
2828
},
2929
};
3030
}
31+
32+
export function useCopyClipboardItems({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<Array<{ mime: string; content: string }>>; text?: string; createToast?: boolean } = {}) {
33+
function toClipboardItem(item: { mime: string; content: string }) {
34+
return new ClipboardItem({
35+
[item.mime]: new Blob([item.content], { type: item.mime }),
36+
});
37+
}
38+
const sourceClipboardItems = computed(() => (toValue(source) || []).map(toClipboardItem));
39+
const { copy, copied, ...rest } = useClipboardItems({
40+
source: sourceClipboardItems,
41+
});
42+
43+
const message = useMessage();
44+
45+
return {
46+
...rest,
47+
isJustCopied: copied,
48+
async copy(content?: { mime: string; content: string }[], { notificationMessage }: { notificationMessage?: string } = {}) {
49+
if (source) {
50+
await copy();
51+
}
52+
else {
53+
await copy((content || []).map(toClipboardItem));
54+
}
55+
56+
if (createToast) {
57+
message.success(notificationMessage ?? text);
58+
}
59+
},
60+
};
61+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue';
3+
import { bundledLanguagesInfo, createHighlighter } from 'shiki/bundle/full';
4+
import { bundledThemesInfo } from 'shiki/themes';
5+
import { useQueryParamOrStorage } from '@/composable/queryParams';
6+
import { useCopy, useCopyClipboardItems } from '@/composable/copy';
7+
8+
const code = ref(`// Using 'typeof' to infer types
9+
const person = { name: "Alice", age: 30 };
10+
type PersonType = typeof person; // { name: string; age: number }
11+
12+
// 'satisfies' to ensure a type matches but allows more specific types
13+
type Animal = { name: string };
14+
const dog = { name: "Buddy", breed: "Golden Retriever" } satisfies Animal;
15+
16+
// Generics with 'extends' and default values
17+
function identity<T extends number | string = string>(arg: T): T {
18+
return arg;
19+
}`);
20+
21+
const themes = ref<{ value: string; label: string }[]>(
22+
bundledThemesInfo.map((item) => {
23+
return {
24+
value: item.id,
25+
label: item.displayName,
26+
};
27+
}));
28+
const langs = ref<{ value: string; label: string }[]>(
29+
bundledLanguagesInfo.map(item => ({
30+
value: item.id,
31+
label: item.name,
32+
})));
33+
34+
const currentTheme = useQueryParamOrStorage({ name: 'theme', storageName: 'code-highlighter:theme', defaultValue: 'dark-plus' });
35+
const currentLang = useQueryParamOrStorage({ name: 'lang', storageName: 'code-highlighter:lang', defaultValue: 'typescript' });
36+
37+
const showLineNumbers = ref(false);
38+
39+
const formattedCodeHtml = computedAsync(async () => {
40+
const currentThemeValue = currentTheme.value;
41+
const currentLangValue = currentLang.value;
42+
const codeValue = code.value;
43+
const needLineNumbers = showLineNumbers.value;
44+
45+
const lineNumberWidth = Math.log10(codeValue.split('\n').length) + 2;
46+
47+
const highlighter = await createHighlighter(
48+
{
49+
langs: [currentLangValue],
50+
themes: [currentThemeValue],
51+
});
52+
return highlighter.codeToHtml(codeValue, {
53+
lang: currentLangValue,
54+
theme: currentThemeValue,
55+
transformers: [
56+
{
57+
postprocess(html: string) {
58+
// when copied to clipboard and pasted to LibreOffice,
59+
// formatting of first line is only kept if there is a line break before...
60+
const ensureFirstLineFormattedWhenCopied
61+
= (html: string) => html.replace('<code>', '<code>\n');
62+
if (!needLineNumbers) {
63+
return ensureFirstLineFormattedWhenCopied(html);
64+
}
65+
let lineNumber = 1;
66+
return html.replace(/<span class="line/g, (m) => {
67+
const lineNumberFormatted = (lineNumber++).toString().padStart(lineNumberWidth, ' ');
68+
return `<span class="line-number" style="white-space-collapse: preserve">${lineNumberFormatted} </span>${m}`;
69+
}).replace('<code>', '<code>\n');
70+
},
71+
},
72+
],
73+
});
74+
});
75+
const htmlClipboardItems = computed(() => [{
76+
mime: 'text/html',
77+
content: formattedCodeHtml.value,
78+
}]);
79+
const { copy: copyHtml } = useCopyClipboardItems({ source: htmlClipboardItems });
80+
const { copy: copyText } = useCopy({ source: code });
81+
</script>
82+
83+
<template>
84+
<div>
85+
<div mb-3 flex items-baseline gap-1>
86+
<c-select
87+
v-model:value="currentLang"
88+
label="Language"
89+
label-position="left"
90+
searchable
91+
:options="langs"
92+
flex-1
93+
/>
94+
<c-select
95+
v-model:value="currentTheme"
96+
label="Theme"
97+
label-position="left"
98+
searchable
99+
:options="themes"
100+
flex-1
101+
/>
102+
</div>
103+
104+
<c-input-text
105+
v-model:value="code"
106+
label="Code snippet to format:"
107+
multiline
108+
placeholder="Put your code snippet here"
109+
rows="5"
110+
mb-3
111+
/>
112+
113+
<div flex justify-center gap-2>
114+
<n-form-item label="Show line numbers" label-placement="left">
115+
<n-switch v-model:value="showLineNumbers" />
116+
</n-form-item>
117+
<c-button @click="copyHtml()">
118+
Copy HTML Formatted
119+
</c-button>
120+
<c-button @click="copyText()">
121+
Copy Code Text
122+
</c-button>
123+
</div>
124+
125+
<div v-html="formattedCodeHtml" /><!-- //NOSONAR -->
126+
</div>
127+
</template>
128+
129+
<style scoped>
130+
::v-deep(.line-number) {
131+
text-wrap: nowrap;
132+
}
133+
</style>

src/tools/code-highlighter/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Code } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Code/Scripts Highlighter',
6+
path: '/code-highlighter',
7+
description: 'Highlight programming code fragments',
8+
keywords: ['code', 'highlighter'],
9+
component: () => import('./code-highlighter.vue'),
10+
icon: Code,
11+
createdAt: new Date('2024-08-15'),
12+
});

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
44
import { tool as emailNormalizer } from './email-normalizer';
55
import { tool as bounceParser } from './bounce-parser';
6+
import { tool as codeHighlighter } from './code-highlighter';
67

78
import { tool as asciiTextDrawer } from './ascii-text-drawer';
89

@@ -187,6 +188,7 @@ export const toolsByCategory: ToolCategory[] = [
187188
xmlFormatter,
188189
yamlViewer,
189190
emailNormalizer,
191+
codeHighlighter,
190192
regexTester,
191193
regexMemo,
192194
],

vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ export default defineConfig({
114114
},
115115
build: {
116116
target: 'esnext',
117+
rollupOptions: {
118+
external: ['regex'],
119+
},
117120
},
118121
optimizeDeps: {
119122
include: ['re2-wasm-embedded'], // optionally specify dependency name

0 commit comments

Comments
 (0)