Skip to content

Commit

Permalink
feat: code highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
dineug committed Dec 2, 2023
1 parent 9182d64 commit b31912b
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/erd-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"@dineug/vite-plugin-r-html": "workspace:*",
"@easylogic/colorpicker": "1.10.11",
"@floating-ui/dom": "1.5.3",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@mdi/js": "7.2.96",
"@radix-ui/colors": "2.1.0",
"@rollup/plugin-typescript": "^11.1.2",
Expand All @@ -71,6 +71,7 @@
"@types/luxon": "^3.3.1",
"@types/ua-parser-js": "^0.7.37",
"color": "^4.2.3",
"comlink": "4.4.1",
"d3": "7.8.5",
"deepmerge": "^4.3.1",
"framer-motion": "^10.16.5",
Expand All @@ -83,6 +84,7 @@
"rxjs": "^7.8.1",
"stats.js": "^0.17.0",
"storybook": "^7.5.3",
"shiki": "0.14.5",
"ts-patch": "^3.0.2",
"tslib": "^2.6.1",
"typescript-transform-paths": "^3.4.6",
Expand Down
6 changes: 3 additions & 3 deletions packages/erd-editor/src/components/erd-editor/ErdEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const ErdEditor: FC<ErdEditorProps, ErdEditorElement> = (props, ctx) => {
const root = createRef<HTMLDivElement>();
useKeyBindingMap(ctx, root);

const { theme, isDarkMode } = useErdEditorAttachElement({
const { theme, hasDarkMode } = useErdEditorAttachElement({
props,
ctx,
app: appContextValue,
Expand Down Expand Up @@ -160,7 +160,7 @@ const ErdEditor: FC<ErdEditorProps, ErdEditorElement> = (props, ctx) => {
class=${[
'root',
styles.root,
{ dark: isDarkMode(), 'none-focus': !state.isFocus },
{ dark: hasDarkMode(), 'none-focus': !state.isFocus },
]}
tabindex="-1"
@keydown=${handleKeydown}
Expand All @@ -176,7 +176,7 @@ const ErdEditor: FC<ErdEditorProps, ErdEditorElement> = (props, ctx) => {
${settings.canvasType === CanvasType.visualization
? html`<${Visualization} />`
: settings.canvasType === CanvasType.schemaSQL
? html`<${SchemaSQL} />`
? html`<${SchemaSQL} isDarkMode=${hasDarkMode()} />`
: null}
</div>
<${ToastContainer} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,6 @@ export function useErdEditorAttachElement({

return {
theme,
isDarkMode: () => themeState.options.appearance === Appearance.dark,
hasDarkMode: () => themeState.options.appearance === Appearance.dark,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ const meta = {
value: {
control: 'text',
},
lang: {
control: 'select',
options: [
'sql',
'typescript',
'graphql',
'csharp',
'java',
'kotlin',
'scala',
],
},
theme: {
control: 'radio',
options: ['dark', 'light'],
},
onCopy: {
action: 'onCopy',
},
Expand All @@ -26,5 +42,7 @@ type Story = StoryObj<CodeBlockProps>;
export const Normal: Story = {
args: {
value: 'SELECT * FROM users;',
lang: 'sql',
theme: 'dark',
},
};
Original file line number Diff line number Diff line change
@@ -1,22 +1,108 @@
import { FC, html, innerHTML } from '@dineug/r-html';
import {
createRef,
FC,
html,
innerHTML,
nextTick,
observable,
onBeforeMount,
ref,
watch,
} from '@dineug/r-html';
import { arrayHas } from '@dineug/shared';

import Icon from '@/components/primitives/icon/Icon';
import { useUnmounted } from '@/hooks/useUnmounted';
import { ShikiService } from '@/utils/shikiService';

import * as styles from './CodeBlock.styles';

const hasPropName = arrayHas<string | number | symbol>([
'value',
'lang',
'theme',
]);

export type CodeBlockProps = {
value: string;
lang: Parameters<ShikiService['codeToHtml']>[1]['lang'];
theme?: 'dark' | 'light';
onCopy?: (value: string) => void;
};

const CodeBlock: FC<CodeBlockProps> = (props, ctx) => {
const root = createRef<HTMLDivElement>();
const { addUnsubscribe } = useUnmounted();

const state = observable({
highlight: '',
backgroundColor: '',
});

const handleCopy = () => {
props.onCopy?.(props.value);
};

const getBackgroundColor = () => {
const $root = root.value;
if (!$root) return null;

const pre = $root.querySelector('pre.shiki') as HTMLPreElement | null;
if (!pre) return null;

const backgroundColor = pre.style.backgroundColor;
if (!backgroundColor) return null;

return backgroundColor;
};

const setBackgroundColor = () => {
nextTick(() => {
state.backgroundColor = getBackgroundColor() || '';
});
};

onBeforeMount(() => {
ShikiService.getInstance()
.codeToHtml(props.value, {
lang: props.lang,
theme: props.theme,
})
.then(highlight => {
state.highlight = highlight;
setBackgroundColor();
});

addUnsubscribe(
watch(props).subscribe(propName => {
if (!hasPropName(propName)) return;

ShikiService.getInstance()
.codeToHtml(props.value, {
theme: props.theme,
lang: props.lang,
})
.then(highlight => {
state.highlight = highlight;
setBackgroundColor();
});
}),
() => {
state.highlight = '';
}
);
});

return () => html`
<div class=${styles.root}>
<pre class=${['scrollbar', styles.code]}>${innerHTML(props.value)}</pre>
<div class=${styles.root} ${ref(root)}>
<div
class=${['scrollbar', styles.code]}
style=${{
'background-color': state.backgroundColor,
}}
>
${innerHTML(state.highlight ? state.highlight : props.value)}
</div>
<div class=${styles.clipboard} title="Copy" @click=${handleCopy}>
<${Icon} prefix="far" name="copy" useTransition=${true} />
</div>
Expand Down
11 changes: 9 additions & 2 deletions packages/erd-editor/src/components/schema-sql/SchemaSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { createSchemaSQL } from '@/utils/schemaSQL';

import * as styles from './SchemaSQL.styles';

export type SchemaSQLProps = {};
export type SchemaSQLProps = {
isDarkMode: boolean;
};

const SchemaSQL: FC<SchemaSQLProps> = (props, ctx) => {
const app = useAppContext(ctx);
Expand Down Expand Up @@ -69,7 +71,12 @@ const SchemaSQL: FC<SchemaSQLProps> = (props, ctx) => {
@contextmenu=${contextMenu.onContextmenu}
@mousedown=${contextMenu.onMousedown}
>
<${CodeBlock} value=${state.sql} .onCopy=${handleCopy} />
<${CodeBlock}
lang="sql"
theme=${props.isDarkMode ? 'dark' : 'light'}
value=${state.sql}
.onCopy=${handleCopy}
/>
<${SchemaSQLContextMenu} .onClose=${handleContextmenuClose} />
</div>
`;
Expand Down
10 changes: 10 additions & 0 deletions packages/erd-editor/src/constants/language.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Language } from '@/constants/schema';

export const LanguageToLangMap: Record<number, string> = {
[Language.TypeScript]: 'typescript',
[Language.GraphQL]: 'graphql',
[Language.csharp]: 'csharp',
[Language.Java]: 'java',
[Language.Kotlin]: 'kotlin',
[Language.Scala]: 'scala',
};
30 changes: 30 additions & 0 deletions packages/erd-editor/src/utils/shikiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as Comlink from 'comlink';

import type { ShikiService as ShikiServiceType } from '@/workers/shiki.worker';
import ShikiWorker from '@/workers/shiki.worker?worker&inline';

class ShikiService {
private static instance: ShikiService;

private worker: Comlink.Remote<ShikiServiceType>;
private constructor() {
this.worker = Comlink.wrap<ShikiServiceType>(new ShikiWorker());
}

static getInstance() {
if (!ShikiService.instance) {
ShikiService.instance = new ShikiService();
}

return ShikiService.instance;
}

async codeToHtml(
...args: Parameters<InstanceType<typeof ShikiServiceType>['codeToHtml']>
) {
const [code, { lang, theme }] = args;
return await this.worker.codeToHtml(code, { lang, theme });
}
}

export { ShikiService };
118 changes: 118 additions & 0 deletions packages/erd-editor/src/workers/shiki.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as Comlink from 'comlink';
import { getHighlighter, setWasm, toShikiTheme } from 'shiki';
// @ts-ignore
import wasmUrl from 'shiki/dist/onig.wasm?url';
import csharp from 'shiki/languages/csharp.tmLanguage.json';
import graphql from 'shiki/languages/graphql.tmLanguage.json';
import java from 'shiki/languages/java.tmLanguage.json';
import kotlin from 'shiki/languages/kotlin.tmLanguage.json';
import scala from 'shiki/languages/scala.tmLanguage.json';
import sql from 'shiki/languages/sql.tmLanguage.json';
import typescript from 'shiki/languages/typescript.tmLanguage.json';
import githubDark from 'shiki/themes/github-dark.json';
import githubLight from 'shiki/themes/github-light.json';

const themeMap: Record<string, any> = {
dark: githubDark,
light: githubLight,
};

const languages: Array<any> = [
{
id: 'typescript',
scopeName: 'source.ts',
displayName: 'TypeScript',
aliases: ['ts'],
grammar: typescript,
},
{
id: 'sql',
scopeName: 'source.sql',
displayName: 'SQL',
grammar: sql,
},
{
id: 'graphql',
scopeName: 'source.graphql',
displayName: 'GraphQL',
aliases: ['gql'],
grammar: graphql,
},
{
id: 'csharp',
scopeName: 'source.cs',
displayName: 'C#',
aliases: ['c#', 'cs'],
grammar: csharp,
},
{
id: 'java',
scopeName: 'source.java',
displayName: 'Java',
grammar: java,
},
{
id: 'kotlin',
scopeName: 'source.kotlin',
displayName: 'Kotlin',
aliases: ['kt', 'kts'],
grammar: kotlin,
},
{
id: 'scala',
scopeName: 'source.scala',
displayName: 'Scala',
grammar: scala,
},
];

function getThemeKey(theme?: string): 'dark' | 'light' {
return theme === 'dark' || theme === 'light' ? theme : 'dark';
}

export class ShikiService {
private ready: Promise<void>;

constructor() {
this.ready = new Promise(async (resolve, reject) => {
try {
const wasmResponse = await fetch(wasmUrl);
setWasm(wasmResponse);
resolve();
} catch (error) {
reject(error);
}
});
}

async codeToHtml(
code: string,
{
lang,
theme,
}: {
lang:
| 'sql'
| 'typescript'
| 'graphql'
| 'csharp'
| 'java'
| 'kotlin'
| 'scala';
theme?: 'dark' | 'light';
}
) {
return await this.ready.then(async () => {
const highlighter = await getHighlighter({
theme: toShikiTheme(Reflect.get(themeMap, getThemeKey(theme))),
langs: languages,
});

return highlighter.codeToHtml(code, { lang });
});
}
}

const service = new ShikiService();

Comlink.expose(service);
Loading

0 comments on commit b31912b

Please sign in to comment.