feat: guess code language

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2024-08-12 20:39:34 +08:00
parent ef0d80d854
commit 03ba85a373
11 changed files with 151 additions and 47 deletions
+1 -2
View File
@@ -52,8 +52,7 @@ export default defineConfig({
excludeReplayWorker: true,
},
moduleMetadata: {
appVersion:
pkg.version,
appVersion: pkg.version,
},
sourcemaps: {
filesToDeleteAfterUpload: ["dist/renderer/assets/*.js.map"],
+1 -1
View File
@@ -10,7 +10,7 @@ export default defineConfig(
lineBreak: "after",
},
lessOpinionated: true,
ignores: ["src/renderer/src/hono.ts", "src/hono.ts"],
ignores: ["src/renderer/src/hono.ts", "src/hono.ts", "resources/**"],
preferESM: false,
},
{
+2 -2
View File
@@ -41,7 +41,7 @@ async function cleanSources(
])
// Keep only node_modules to be included in the app
const modules = new Set(["font-list"])
const modules = new Set(["font-list", "@vscode/vscode-languagedetection"])
await Promise.all([
...(await readdir(buildPath).then((items) =>
items
@@ -70,7 +70,7 @@ const config: ForgeConfig = {
],
afterCopy: [cleanSources],
asar: true,
ignore: [/^\/node_modules\/(?!font-list)/],
ignore: [/^\/node_modules\/(?!font-list|@vscode\/vscode-languagedetection)/],
prune: true,
...(process.env.APPLE_ID &&
process.env.APPLE_PASSWORD &&
+1
View File
@@ -69,6 +69,7 @@
"@tanstack/react-query-devtools": "5.51.15",
"@tanstack/react-query-persist-client": "5.51.15",
"@use-gesture/react": "10.3.1",
"@vscode/vscode-languagedetection": "1.0.22",
"@yornaath/batshit": "0.10.1",
"builder-util-runtime": "9.2.5-alpha.3",
"class-variance-authority": "0.7.0",
File diff suppressed because one or more lines are too long
+9
View File
@@ -139,6 +139,9 @@ importers:
'@use-gesture/react':
specifier: 10.3.1
version: 10.3.1(react@18.3.1)
'@vscode/vscode-languagedetection':
specifier: 1.0.22
version: 1.0.22
'@yornaath/batshit':
specifier: 0.10.1
version: 0.10.1
@@ -2730,6 +2733,10 @@ packages:
'@vitest/utils@2.0.4':
resolution: {integrity: sha512-Zc75QuuoJhOBnlo99ZVUkJIuq4Oj0zAkrQ2VzCqNCx6wAwViHEh5Fnp4fiJTE9rA+sAoXRf00Z9xGgfEzV6fzQ==}
'@vscode/vscode-languagedetection@1.0.22':
resolution: {integrity: sha512-rQ/BgMyLuIXSmbA0MSkIPHtcOw14QkeDbAq19sjvaS9LTRr905yij0S8lsyqN5JgOsbtIx7pAcyOxFMzPmqhZQ==}
hasBin: true
'@xmldom/xmldom@0.8.10':
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
@@ -9917,6 +9924,8 @@ snapshots:
loupe: 3.1.1
tinyrainbow: 1.2.0
'@vscode/vscode-languagedetection@1.0.22': {}
'@xmldom/xmldom@0.8.10': {}
'@yornaath/batshit-devtools@1.7.1': {}
+10 -2
View File
@@ -16,11 +16,19 @@ export const env = createEnv({
},
emptyStringAsUndefined: true,
runtimeEnv:
"process" in globalThis ? process.env : injectExternalEnv(import.meta.env),
runtimeEnv: getRuntimeEnv(),
skipValidation: !isDev,
})
function getRuntimeEnv() {
try {
return injectExternalEnv(import.meta.env)
} catch {
return process.env
}
}
function injectExternalEnv<T>(originEnv: T): T {
if (!("document" in globalThis)) {
return originEnv
-2
View File
@@ -1,5 +1,3 @@
import "dotenv/config"
import { electronApp, optimizer } from "@electron-toolkit/utils"
import { APP_PROTOCOL, DEEPLINK_SCHEME } from "@shared/constants"
import { extractElectronWindowOptions } from "@shared/electron"
+11
View File
@@ -1,6 +1,9 @@
import { createRequire } from "node:module"
import { readability } from "../lib/readability"
import { t } from "./_instance"
const require = createRequire(import.meta.url)
export const readerRoute = {
readability: t.procedure
.input<{ url: string }>()
@@ -12,6 +15,14 @@ export const readerRoute = {
}
const result = await readability(url)
return result
}),
detectCodeStringLanguage: t.procedure
.input<{ codeString: string }>()
.action(async ({ input }) => {
const { ModelOperations } = require("@vscode/vscode-languagedetection")
const modulOperations = new ModelOperations()
const result = await modulOperations.runModel(input.codeString)
return result
}),
}
@@ -1,7 +1,14 @@
import { useUISettingSelector } from "@renderer/atoms/settings/ui"
import { tipcClient } from "@renderer/lib/client"
import { cn } from "@renderer/lib/utils"
import type { FC } from "react"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import {
useInsertionEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import type {
BundledLanguage,
BundledTheme,
@@ -16,10 +23,7 @@ import { shikiTransformers } from "./shared"
import styles from "./shiki.module.css"
const shiki = await createHighlighterCore({
themes: [
import("shiki/themes/github-dark.mjs"),
],
themes: [import("shiki/themes/github-dark.mjs")],
langs: [],
loadWasm: getWasm,
})
@@ -42,15 +46,49 @@ let langModule: Record<
> | null = null
let themeModule: Record<BundledTheme, DynamicImportThemeRegistration> | null =
null
let bundledLanguagesKeysSet: Set<string> | null = null
export const ShikiHighLighter: FC<ShikiProps> = (props) => {
const { code, language, className, theme: overrideTheme } = props
const [currentLanguage, setCurrentLanguage] = useState(
language || "plaintext",
)
useInsertionEffect(() => {
if (language || !ELECTRON) return
if (!bundledLanguagesKeysSet) {
import("shiki/langs")
.then(({ bundledLanguages }) => {
langModule = bundledLanguages
bundledLanguagesKeysSet = new Set(Object.keys(bundledLanguages))
})
.then(guessLanguage)
} else {
guessLanguage()
}
function guessLanguage() {
tipcClient
?.detectCodeStringLanguage({ codeString: code })
.then((result) => {
for (const item of result) {
if (bundledLanguagesKeysSet?.has(item.languageId)) {
setCurrentLanguage(item.languageId)
break
}
}
})
}
}, [])
const loadThemesRef = useRef([] as string[])
const loadLanguagesRef = useRef([] as string[])
const [loaded, setLoaded] = useState(false)
const codeTheme = useUISettingSelector((s) => overrideTheme || s.codeHighlightTheme)
const codeTheme = useUISettingSelector(
(s) => overrideTheme || s.codeHighlightTheme,
)
useLayoutEffect(() => {
let isMounted = true
setLoaded(false)
@@ -69,7 +107,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
}
async function register() {
if (!language || !codeTheme) return
if (!currentLanguage || !codeTheme) return
const [{ bundledLanguages }, { bundledThemes }] =
langModule && themeModule ?
@@ -85,20 +123,20 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
themeModule = bundledThemes
if (
language &&
loadLanguagesRef.current.includes(language) &&
currentLanguage &&
loadLanguagesRef.current.includes(currentLanguage) &&
codeTheme &&
(loadThemesRef.current.includes(codeTheme))
loadThemesRef.current.includes(codeTheme)
) {
return
}
return Promise.all([
(async () => {
if (language) {
const importFn = (bundledLanguages as any)[language]
if (currentLanguage) {
const importFn = (bundledLanguages as any)[currentLanguage]
if (!importFn) return
await loadShikiLanguage(language || "", importFn)
loadLanguagesRef.current.push(language)
await loadShikiLanguage(currentLanguage || "", importFn)
loadLanguagesRef.current.push(currentLanguage)
}
})(),
(async () => {
@@ -119,7 +157,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
return () => {
isMounted = false
}
}, [codeTheme, language])
}, [codeTheme, currentLanguage])
if (!loaded) {
return (
@@ -128,13 +166,16 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
</pre>
)
}
return <ShikiCode {...props} codeTheme={codeTheme} />
return (
<ShikiCode {...props} language={currentLanguage} codeTheme={codeTheme} />
)
}
const ShikiCode: FC<ShikiProps & {
codeTheme: string
}> = ({ code, language, codeTheme, className, transparent }) => {
const ShikiCode: FC<
ShikiProps & {
codeTheme: string
}
> = ({ code, language, codeTheme, className, transparent }) => {
const rendered = useMemo(() => {
try {
return shiki.codeToHtml(code, {
@@ -159,9 +200,19 @@ const ShikiCode: FC<ShikiProps & {
)
}
return (
<div className={cn("group relative my-4", styles["shiki-wrapper"], transparent ? styles["transparent"] : null, className)}>
<div
className={cn(
"group relative my-4",
styles["shiki-wrapper"],
transparent ? styles["transparent"] : null,
className,
)}
>
<div dangerouslySetInnerHTML={{ __html: rendered }} />
<CopyButton value={code} className="absolute right-1 top-1 opacity-0 duration-200 group-hover:opacity-100" />
<CopyButton
value={code}
className="absolute right-1 top-1 opacity-0 duration-200 group-hover:opacity-100"
/>
</div>
)
}
+33 -15
View File
@@ -92,14 +92,13 @@ export const parseHtml = async (
pre: ({ node, ...props }) => {
if (!props.children) return null
let language = "plaintext"
let language = ""
let codeString = null as string | null
if (props.className?.includes("language-")) {
language = props.className.replace("language-", "")
}
if (typeof props.children !== "object") {
language = "plaintext"
codeString = props.children.toString()
} else {
if (
@@ -115,9 +114,8 @@ export const parseHtml = async (
const code =
"props" in props.children && props.children.props.children
if (!code) return null
const $text = document.createElement("div")
$text.innerHTML = renderToString(code)
codeString = $text.textContent
codeString = extractCodeFromHtml(renderToString(code))
}
if (!codeString) return null
@@ -127,17 +125,18 @@ export const parseHtml = async (
language: language.toLowerCase(),
})
},
table: ({ node, ...props }) => createElement(
"div",
{
className: "w-full overflow-x-auto",
},
table: ({ node, ...props }) =>
createElement(
"div",
{
className: "w-full overflow-x-auto",
},
createElement("table", {
...props,
className: tw`w-full my-0`,
}),
),
createElement("table", {
...props,
className: tw`w-full my-0`,
}),
),
},
}),
}
@@ -161,3 +160,22 @@ const Img: Components["img"] = ({ node, ...props }) => {
return createElement(MarkdownBlockImage, nextProps)
}
function extractCodeFromHtml(htmlString: string) {
const tempDiv = document.createElement("div")
tempDiv.innerHTML = htmlString
const divElements = tempDiv.querySelectorAll("div")
let code = ""
divElements.forEach((div) => {
code += `${div.textContent}\n`
})
if (divElements.length === 0) {
return tempDiv.textContent
}
return code
}