From 644917ee45e791500b75c92acec4756faf721951 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 26 Jul 2024 18:28:05 +0800 Subject: [PATCH] ui: add highlight preview --- .../components/highlighter/shiki.ts | 39 ++-- .../ui-default/pages/home_preference.page.jsx | 72 ------- .../ui-default/pages/home_preference.page.tsx | 200 ++++++++++++++++++ packages/ui-default/setting.yaml | 10 + .../ui-default/templates/components/form.html | 3 + .../templates/partials/setting.html | 10 +- 6 files changed, 238 insertions(+), 96 deletions(-) delete mode 100644 packages/ui-default/pages/home_preference.page.jsx create mode 100644 packages/ui-default/pages/home_preference.page.tsx diff --git a/packages/ui-default/components/highlighter/shiki.ts b/packages/ui-default/components/highlighter/shiki.ts index b0e77cf5a..ea05c88b5 100644 --- a/packages/ui-default/components/highlighter/shiki.ts +++ b/packages/ui-default/components/highlighter/shiki.ts @@ -8,7 +8,7 @@ import Notification from 'vj/components/notification/index'; import { getFeatures, load as loadModule } from 'vj/lazyload'; import { i18n } from 'vj/utils'; import { - addLanguage, bundledLanguages, bundledLanguagesInfo, createHighlighter, + addLanguage, bundledLanguages, bundledLanguagesInfo, bundledThemes, createHighlighter, } from './bundle'; $(document).on('click', '[data-clipboard-text]', function (ev) { @@ -22,21 +22,26 @@ $(document).on('click', '[data-clipboard-text]', function (ev) { ev.preventDefault(); }); -const baseTransformers = [ - transformerNotationDiff(), - transformerNotationHighlight(), - transformerNotationWordHighlight(), - transformerMetaHighlight(), -]; +function getTransformers() { + const baseTransformers = [ + transformerNotationDiff(), + transformerNotationHighlight(), + transformerNotationWordHighlight(), + transformerMetaHighlight(), + ]; -if (UserContext.showInvisibleChar) { - baseTransformers.push(transformerRenderWhitespace()); + if (UserContext.showInvisibleChar) { + baseTransformers.push(transformerRenderWhitespace()); + } + return baseTransformers; } +let theme = UiContext.highlightTheme; +if (!bundledThemes[theme]) theme = 'vitesse-light'; async function highlightBlocks($dom) { const nodes = $dom.find('pre code').get(); if (!nodes.length) return; - const shikiPlain = await createHighlighter({ langs: ['text'], themes: ['github-dark', 'light-plus'] }); + const shikiPlain = await createHighlighter({ langs: ['text'], themes: [theme] }); const requestedLanguages = new Set([]); const tasks = []; for (const code of nodes) { @@ -91,7 +96,7 @@ async function highlightBlocks($dom) { }, ], }; - const transformers = baseTransformers.concat({ + const transformers = getTransformers().concat({ pre(node) { this.addClassToHast(node, 'syntax-hl'); if (language.includes('line-numbers')) { @@ -102,7 +107,7 @@ async function highlightBlocks($dom) { }); const $plain = $(shikiPlain.codeToHtml($code.text(), { lang: 'text', - theme: 'light-plus', + theme, meta: { __raw: m[2] ? `{${m[2]}}` : null, }, @@ -114,7 +119,7 @@ async function highlightBlocks($dom) { if (!bundledLanguages[name]) return; $plain.replaceWith($(shiki.codeToHtml($code.text(), { lang: name, - theme: 'light-plus', + theme, meta: { __raw: m[2] ? `{${m[2]}}` : null, }, @@ -137,17 +142,19 @@ async function highlightBlocks($dom) { } if (!bundledLanguages[name]) requestedLanguages.delete(name); } - const shiki = await createHighlighter({ langs: Array.from(requestedLanguages), themes: ['github-dark', 'light-plus'] }); + const shiki = await createHighlighter({ langs: Array.from(requestedLanguages), themes: [theme] }); for (const task of tasks) task(shiki); } async function highlight(text: string, grammar, language) { - const shiki = await createHighlighter({ langs: [language], themes: ['github-dark', 'light-plus'] }); - return shiki.codeToHtml(text, { lang: language, theme: 'light-plus' }); + const shiki = await createHighlighter({ langs: [language], themes: [theme] }); + return shiki.codeToHtml(text, { lang: language, theme, transformers: getTransformers() }); } export { + bundledThemes, highlightBlocks, highlight, createHighlighter, + getTransformers, }; diff --git a/packages/ui-default/pages/home_preference.page.jsx b/packages/ui-default/pages/home_preference.page.jsx deleted file mode 100644 index d9a6f410d..000000000 --- a/packages/ui-default/pages/home_preference.page.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import $ from 'jquery'; -import { renderLanguageSelect } from 'vj/components/languageselect'; -import { NamedPage } from 'vj/misc/Page'; -import { - delay, getAvailableLangs, i18n, tpl, -} from 'vj/utils'; - -async function initCodeLangHelper() { - const $el = $(tpl`
`); - $('[name="codeLang"]') - .parent().parent().parent() - .parent() - .hide() - .after($el); - - const main = {}; - for (const key in window.LANGS) { - if (!key.includes('.') && !window.LANGS[key].hidden) main[key] = window.LANGS[key].display; - } - - await delay(50); - renderLanguageSelect( - document.getElementById('codelang-select'), - $('[name="codeLang"]'), - getAvailableLangs(), - main, - [$('[name="codeLang"]').val()], - ); -} - -function supportFontFamily(f) { - const h = 'Arial'; - if (f.toLowerCase() === h.toLowerCase()) return true; - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const ctx = canvas.getContext('2d'); - ctx.textAlign = 'center'; - ctx.fillStyle = 'black'; - ctx.textBaseline = 'middle'; - const g = (j) => { - ctx.clearRect(0, 0, 100, 100); - ctx.font = `100px ${j}, ${h}`; - ctx.fillText('a', 50, 50); - const k = ctx.getImageData(0, 0, 100, 100).data; - return [].slice.call(k).filter((l) => l !== 0); - }; - return g(h).join('') !== g(f).join(''); -} - -const page = new NamedPage('home_preference', () => { - initCodeLangHelper(); - $('[name="fontFamily"] option, [name="codeFontFamily"] option').each(function () { - if (!supportFontFamily(this.value)) { - $(this).hide(); - console.log(`Unsupported: ${this.value}`); - } - this.style.fontFamily = this.getAttribute('value'); - this.textContent = i18n(this.textContent.trim()); - }); - function updateFont() { - this.style.fontFamily = $(this).val(); - } - document.fonts.onloadingdone = () => { - $('[name="fontFamily"] option, [name="codeFontFamily"] option').each(function () { - if (supportFontFamily(this.value)) $(this).show(); - }); - }; - $('[name="fontFamily"], [name="codeFontFamily"]').on('change', updateFont).each(updateFont); -}); - -export default page; diff --git a/packages/ui-default/pages/home_preference.page.tsx b/packages/ui-default/pages/home_preference.page.tsx new file mode 100644 index 000000000..7d0834db3 --- /dev/null +++ b/packages/ui-default/pages/home_preference.page.tsx @@ -0,0 +1,200 @@ +import $ from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { renderLanguageSelect } from 'vj/components/languageselect'; +import Dom from 'vj/components/react/DomComponent'; +import { NamedPage } from 'vj/misc/Page'; +import { + delay, getAvailableLangs, i18n, tpl, +} from 'vj/utils'; + +async function initCodeLangHelper() { + const $el = $(tpl`
`); + $('[name="codeLang"]') + .parent().parent().parent() + .parent() + .hide() + .after($el); + + const main = {}; + for (const key in window.LANGS) { + if (!key.includes('.') && !window.LANGS[key].hidden) main[key] = window.LANGS[key].display; + } + + await delay(50); + renderLanguageSelect( + document.getElementById('codelang-select'), + $('[name="codeLang"]'), + getAvailableLangs(), + main, + [$('[name="codeLang"]').val()], + ); +} + +function supportFontFamily(f) { + const h = 'Arial'; + if (f.toLowerCase() === h.toLowerCase()) return true; + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const ctx = canvas.getContext('2d'); + ctx.textAlign = 'center'; + ctx.fillStyle = 'black'; + ctx.textBaseline = 'middle'; + const g = (j) => { + ctx.clearRect(0, 0, 100, 100); + ctx.font = `100px ${j}, ${h}`; + ctx.fillText('a', 50, 50); + const k = ctx.getImageData(0, 0, 100, 100).data; + return [].slice.call(k).filter((l) => l !== 0); + }; + return g(h).join('') !== g(f).join(''); +} + +interface HighlightProps { + theme: string; + setAvailableThemes: (themes: string[]) => void; +} +function HighlightPreview(props: HighlightProps) { + const [ready, setReady] = React.useState(false); + const [html, setHtml] = React.useState(tpl`
`); + const [code, setCode] = React.useState(''); + const [module, setShikiModule] = React.useState(null); + const [rerender, setRerender] = React.useState(0); + + React.useEffect(() => { + $('[name="showInvisibleChar"]').on('change', (ev) => { + UserContext.showInvisibleChar = ev.target.checked; + setRerender((i) => i + 1); + }); + Promise.all([ + import('vj/components/highlighter/shiki'), + import('vj/components/highlighter/code-example'), + ]).then(([m, { default: c }]) => { + setShikiModule(m); + setCode(c); + props.setAvailableThemes(Object.keys(m.bundledThemes)); + }); + }, []); + React.useEffect(() => { + setReady(false); + let theme = props.theme; + if (!module?.bundledThemes?.[theme]) theme = 'light-plus'; + module?.createHighlighter({ langs: ['cpp'], themes: [theme] }).then((shiki) => { + setHtml(shiki.codeToHtml(code, { lang: 'cpp', theme, transformers: module.getTransformers() })); + setReady(true); + }); + }, [props.theme, module, code, rerender]); + return
+ {(!ready) &&
} +
+
; +} + +const page = new NamedPage('home_preference', () => { + const div = document.createElement('div'); + + function App() { + const [highlightTheme, setHighlightTheme] = React.useState(UserContext.highlightTheme); + const [themes, setAvailableThemes] = React.useState([]); + + React.useEffect(() => { + // document.fonts.onloadingdone = () => setProbeFont((i) => i + 1); + $('[name="highlightTheme"]').on('change', (ev) => { + setHighlightTheme(ev.target.value); + }); + }, []); + React.useEffect(() => { + UserContext.highlightTheme = highlightTheme; + }, [highlightTheme]); + + return
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+

test

+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ {themes.length ? + : } +
+
+ Loading preview

}> + +
+
+
; + } + + $('form[method="post"]').prepend(div); + ReactDOM.createRoot(div).render(); + initCodeLangHelper(); + $('[name="fontFamily"] option, [name="codeFontFamily"] option').each(function () { + if (!supportFontFamily(this.value)) { + $(this).hide(); + console.log(`Unsupported: ${this.value}`); + } + this.style.fontFamily = this.getAttribute('value'); + this.textContent = i18n(this.textContent.trim()); + }); + function updateFont() { + this.style.fontFamily = $(this).val(); + } + document.fonts.onloadingdone = () => { + $('[name="fontFamily"] option, [name="codeFontFamily"] option').each(function () { + if (supportFontFamily(this.value)) $(this).show(); + }); + }; + $('[name="fontFamily"], [name="codeFontFamily"]').on('change', updateFont).each(updateFont); +}); + +export default page; diff --git a/packages/ui-default/setting.yaml b/packages/ui-default/setting.yaml index 58fb242a7..482cebd9a 100644 --- a/packages/ui-default/setting.yaml +++ b/packages/ui-default/setting.yaml @@ -67,6 +67,16 @@ showInvisibleChar: family: highlight desc: show invisible characters when formatting code category: preference +codeFontLigatures: + type: boolean + family: highlight + desc: fontLigatures + category: preference +highlightTheme: + type: text + family: highlight + desc: Highlight Theme + category: preference formatCode: type: boolean family: highlight diff --git a/packages/ui-default/templates/components/form.html b/packages/ui-default/templates/components/form.html index a1a65b8a4..2ee5d5db5 100644 --- a/packages/ui-default/templates/components/form.html +++ b/packages/ui-default/templates/components/form.html @@ -209,5 +209,8 @@ {% macro form_checkbox(args) %} {{ form_begin({columns:5}|assign(args)) }} {{ checkbox(args) }} +{% if args.boolean_placeholder %} + +{% endif %} {{ form_end(args) }} {% endmacro %} diff --git a/packages/ui-default/templates/partials/setting.html b/packages/ui-default/templates/partials/setting.html index 436793c6c..f93c92015 100644 --- a/packages/ui-default/templates/partials/setting.html +++ b/packages/ui-default/templates/partials/setting.html @@ -72,14 +72,8 @@ placeholder:setting.desc, name:setting.key, value:setting.value if (_val === undefined or _val === null) else _val, - disabled:setting.flag|bitand(2) - })}} - {{ form.form_checkbox({ - label:setting.name, - name:'booleanKeys.' + setting.key, - value:true, - extra_class:'display-hidden', - no_label:true + disabled:setting.flag|bitand(2), + boolean_placeholder: true })}} {% endif %} {% endif %}