Skip to content

Commit 91e3d07

Browse files
committed
feat(new tool): Code Highlighter
Fix CorentinTh#1280
1 parent 318fb6e commit 91e3d07

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Tool - Code highlighter', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/code-highlighter');
6+
});
7+
8+
test('Has correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('Code highlighter - IT Tools');
10+
});
11+
12+
test('', async ({ page }) => {
13+
14+
});
15+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue';
3+
import { VueShikiInput, fetchShikiBundles } from 'vue-shiki-input';
4+
import shiki from 'shiki';
5+
import 'vue-shiki-input/style.css';
6+
import { useQueryParamOrStorage } from '@/composable/queryParams';
7+
import { useCopy, useCopyClipboardItems } from '@/composable/copy';
8+
9+
const text = ref('');
10+
11+
const shikiLoading = ref(false);
12+
13+
const themes = ref<{ id: string; displayName: string; type: 'dark' | 'light' }[]>([]);
14+
const langs = ref<{ id: string; name: string }[]>([]);
15+
16+
const currentTheme = useQueryParamOrStorage({ name: 'theme', storageName: 'code-highlighter:theme', defaultValue: 'dark-plus' });
17+
const themeDark = computed(() => {
18+
if (!currentTheme.value) {
19+
return false;
20+
}
21+
const index = themes.value.findIndex(theme => theme.id === currentTheme.value);
22+
return themes.value[index].type === 'dark';
23+
});
24+
const isDarkTheme = useDark();
25+
26+
watch(themeDark, isDark => isDarkTheme.value = isDark);
27+
28+
const currentLang = useQueryParamOrStorage({ name: 'lang', storageName: 'code-highlighter:lang', defaultValue: 'typescript' });
29+
const showLineNumbers = ref(false);
30+
31+
(async () => {
32+
const { bundledLanguagesInfo, bundledThemesInfo } = await fetchShikiBundles();
33+
themes.value = bundledThemesInfo.map(item => ({
34+
value: item.id,
35+
label: item.displayName,
36+
type: item.type,
37+
}));
38+
langs.value = bundledLanguagesInfo.map(item => ({
39+
value: item.id,
40+
label: item.name,
41+
}));
42+
})();
43+
44+
const htmlClipboardItem = computedAsync(async () => {
45+
const currentThemeValue = currentTheme.value;
46+
const currentLangValue = currentLang.value;
47+
const textValue = text.value;
48+
49+
const highlighter = await shiki.getHighlighter({
50+
themes: [currentThemeValue],
51+
langs: [currentLangValue],
52+
});
53+
const code = highlighter.codeToHtml(textValue, currentLangValue, currentThemeValue);
54+
return [{
55+
mime: 'text/html',
56+
content: code,
57+
}];
58+
});
59+
const { copy: copyHtml } = useCopyClipboardItems({ source: htmlClipboardItem });
60+
const { copy: copyText } = useCopy({ source: text });
61+
62+
function downloadBlob(blob: Blob, name: string) {
63+
// Create a URL for the Blob
64+
const url = URL.createObjectURL(blob);
65+
66+
// Create an anchor element to trigger the download
67+
const a = document.createElement('a');
68+
a.href = url;
69+
// Set file name
70+
a.download = name;
71+
a.style.display = 'none';
72+
document.body.appendChild(a);
73+
a.click();
74+
75+
// Clean up by removing the anchor and revoking the URL
76+
document.body.removeChild(a);
77+
URL.revokeObjectURL(url);
78+
}
79+
80+
async function exportSVG() {
81+
const currentThemeValue = currentTheme.value;
82+
const currentLangValue = currentLang.value;
83+
const textValue = text.value;
84+
85+
const highlighter = await shiki.getHighlighter({
86+
themes: [currentThemeValue],
87+
langs: [currentLangValue],
88+
});
89+
const renderer = await shiki.getSVGRenderer({
90+
fontFamily: 'monospace',
91+
fontSize: 14,
92+
});
93+
94+
const tokens = highlighter.codeToThemedTokens(textValue, currentLangValue, currentThemeValue);
95+
const blob = new Blob(
96+
[
97+
renderer.renderToSVG(tokens, {
98+
bg: highlighter.getBackgroundColor(currentThemeValue),
99+
}),
100+
],
101+
{ type: 'image/svg+xml;charset=utf-8' },
102+
);
103+
104+
downloadBlob(blob, `${currentLangValue}-${currentThemeValue}.svg`);
105+
}
106+
</script>
107+
108+
<template>
109+
<div>
110+
<n-form-item label="Show line numbers" label-placement="left">
111+
<n-switch v-model:value="showLineNumbers" />
112+
</n-form-item>
113+
<c-select
114+
v-model:value="currentLang"
115+
label="Language"
116+
label-position="left"
117+
searchable
118+
:options="langs"
119+
mb-2
120+
/>
121+
<c-select
122+
v-model:value="currentTheme"
123+
label="Theme"
124+
label-position="left"
125+
searchable
126+
:options="themes"
127+
mb-2
128+
/>
129+
130+
<div flex justify-center>
131+
<c-button @click="copyHtml()">
132+
Copy HTML Formatted
133+
</c-button>
134+
<c-button @click="copyText()">
135+
Copy Code Text
136+
</c-button>
137+
</div>
138+
<VueShikiInput
139+
v-model="text"
140+
v-model:loading="shikiLoading"
141+
:code-to-html-options="{
142+
lang: currentLang!,
143+
theme: currentTheme!,
144+
}"
145+
:offset="{
146+
x: 10,
147+
y: 10,
148+
}"
149+
:line-numbers="showLineNumbers"
150+
auto-background focus
151+
/>
152+
</div>
153+
</template>

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
@@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter';
22
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';
5+
import { tool as codeHighlighter } from './code-highlighter';
56

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

@@ -154,6 +155,7 @@ export const toolsByCategory: ToolCategory[] = [
154155
xmlFormatter,
155156
yamlViewer,
156157
emailNormalizer,
158+
codeHighlighter,
157159
],
158160
},
159161
{

0 commit comments

Comments
 (0)