diff --git a/packages/ui-default/components/highlighter/highlighter.page.styl b/packages/ui-default/components/highlighter/highlighter.page.styl index de75ef012..5bf46eaf9 100644 --- a/packages/ui-default/components/highlighter/highlighter.page.styl +++ b/packages/ui-default/components/highlighter/highlighter.page.styl @@ -89,6 +89,7 @@ code border-radius: 0px !important background: none !important box-shadow: 0 2px #0003 + text-decoration: none padding: 0 .5em color: #bbb diff --git a/packages/ui-default/components/highlighter/shiki.ts b/packages/ui-default/components/highlighter/shiki.ts index c12b41403..b0e77cf5a 100644 --- a/packages/ui-default/components/highlighter/shiki.ts +++ b/packages/ui-default/components/highlighter/shiki.ts @@ -2,11 +2,14 @@ import { transformerMetaHighlight, transformerNotationDiff, transformerNotationHighlight, transformerNotationWordHighlight, transformerRenderWhitespace, } from '@shikijs/transformers'; +import type { ElementContent } from 'hast'; import $ from 'jquery'; import Notification from 'vj/components/notification/index'; import { getFeatures, load as loadModule } from 'vj/lazyload'; import { i18n } from 'vj/utils'; -import { addLanguage, bundledLanguages, createHighlighter } from './bundle'; +import { + addLanguage, bundledLanguages, bundledLanguagesInfo, createHighlighter, +} from './bundle'; $(document).on('click', '[data-clipboard-text]', function (ev) { const content = $(this).attr('data-clipboard-text'); @@ -19,7 +22,7 @@ $(document).on('click', '[data-clipboard-text]', function (ev) { ev.preventDefault(); }); -const transformers = [ +const baseTransformers = [ transformerNotationDiff(), transformerNotationHighlight(), transformerNotationWordHighlight(), @@ -27,20 +30,101 @@ const transformers = [ ]; if (UserContext.showInvisibleChar) { - transformers.push(transformerRenderWhitespace()); + baseTransformers.push(transformerRenderWhitespace()); } async function highlightBlocks($dom) { - for (const code of $dom.find('pre code').get()) { + const nodes = $dom.find('pre code').get(); + if (!nodes.length) return; + const shikiPlain = await createHighlighter({ langs: ['text'], themes: ['github-dark', 'light-plus'] }); + const requestedLanguages = new Set([]); + const tasks = []; + for (const code of nodes) { const $code = $(code); const $pre = $code.parent(); if ($pre.hasClass('syntax-hl')) continue; const language = ($(code).attr('class') || '').trim(); // try to map the language name - const m = language.match(/language-([a-z0-9]+)(?=\.line-numbers)?(?=\|([\d,-]+))?/); + const m = language.match(/language-([a-zA-Z0-9]+)(?=\.line-numbers)?(?=\|([\d,-]+))?/); if (!m) continue; - if (!bundledLanguages[m[1]]) { - const features = await getFeatures(`shiki-${m[1]}`); + const name = m[1].toLowerCase(); + const toolbarContents: ElementContent[] = [ + { + tagName: 'a', + type: 'element', + properties: { + href: 'javascript:;', + class: 'copy-to-clipboard', + 'data-clipboard-text': $code.text(), + }, + children: [ + { type: 'text', value: 'Copy' }, + ], + }, + ]; + if (!name.startsWith('input') && !name.startsWith('output')) { + toolbarContents.unshift({ + tagName: 'span', + type: 'element', + properties: { + style: 'padding: 0 10px 0 10px', + }, + children: [ + { type: 'text', value: bundledLanguagesInfo[m[1]]?.name || m[1] }, + ], + }); + } + const toolbar: ElementContent = { + tagName: 'div', + type: 'element', + properties: { + class: 'toolbar', + }, + children: [ + { + tagName: 'div', + type: 'element', + properties: { + class: 'toolbar-item', + }, + children: toolbarContents, + }, + ], + }; + const transformers = baseTransformers.concat({ + pre(node) { + this.addClassToHast(node, 'syntax-hl'); + if (language.includes('line-numbers')) { + this.addClassToHast(node, 'line-numbers'); + } + node.children.push(toolbar); + }, + }); + const $plain = $(shikiPlain.codeToHtml($code.text(), { + lang: 'text', + theme: 'light-plus', + meta: { + __raw: m[2] ? `{${m[2]}}` : null, + }, + transformers, + })); + $pre.replaceWith($plain); + requestedLanguages.add(name); + tasks.push((shiki) => { + if (!bundledLanguages[name]) return; + $plain.replaceWith($(shiki.codeToHtml($code.text(), { + lang: name, + theme: 'light-plus', + meta: { + __raw: m[2] ? `{${m[2]}}` : null, + }, + transformers, + }))); + }); + } + for (const name of requestedLanguages) { + if (!bundledLanguages[name]) { + const features = await getFeatures(`shiki-${name}`); for (const item of features) { let apply = typeof item === 'function' ? item @@ -51,57 +135,10 @@ async function highlightBlocks($dom) { if (typeof apply === 'function') await apply(addLanguage); } } - const highlightLang = bundledLanguages[m[1]] ? m[1] : 'none'; - const shiki = await createHighlighter({ - langs: [highlightLang], - themes: ['github-dark', 'light-plus'], - }); - $pre.replaceWith($(shiki.codeToHtml($code.text(), { - lang: highlightLang, - theme: 'light-plus', - meta: { - __raw: m[2] ? `{${m[2]}}` : null, - }, - transformers: transformers.concat({ - pre(node) { - this.addClassToHast(node, 'syntax-hl'); - if (language.includes('line-numbers')) { - this.addClassToHast(node, 'line-numbers'); - } - node.children.push({ - tagName: 'div', - type: 'element', - properties: { - class: 'toolbar', - }, - children: [ - { - tagName: 'div', - type: 'element', - properties: { - class: 'toolbar-item', - }, - children: [ - { - tagName: 'a', - type: 'element', - properties: { - href: 'javascript:;', - class: 'copy-to-clipboard', - 'data-clipboard-text': $code.text(), - }, - children: [ - { type: 'text', value: 'Copy' }, - ], - }, - ], - }, - ], - }); - }, - }), - }))); + if (!bundledLanguages[name]) requestedLanguages.delete(name); } + const shiki = await createHighlighter({ langs: Array.from(requestedLanguages), themes: ['github-dark', 'light-plus'] }); + for (const task of tasks) task(shiki); } async function highlight(text: string, grammar, language) {