ui: switch to shiki

This commit is contained in:
undefined committed 2024-07-25 02:36:53 +08:00
1 parent 41a949348b
commit b61177f08b
7 files changed
+173 -150

No files matched your search

@@ -1,11 +1,9 @@
@import 'prism-themes/themes/prism-vs.css'
pre, code pre, code
background: $syntax-hl-bg-color background: $syntax-hl-bg-color
pre > code pre > code
background: none background: none
line-height: 1.5 line-height: 1.2
color: black color: black
text-align: left text-align: left
white-space: pre-wrap white-space: pre-wrap
@@ -19,11 +17,16 @@ pre
padding: rem(5px) padding: rem(5px)
margin-bottom: rem(20px) margin-bottom: rem(20px)
pre.shiki
padding: 1em
margin: .5em 0
border: 1px solid #dddddd
code code
color: $code-color color: $code-color
font-size: rem($font-size-secondary) font-size: rem($font-size-secondary)
.line-highlight .shiki .line.highlighted
position: absolute position: absolute
left: 0 left: 0
right: 0 right: 0
@@ -35,6 +38,33 @@ code
line-height: inherit line-height: inherit
white-space: pre white-space: pre
.line-numbers code
counter-reset: step;
counter-increment: step 0;
.line::before
content: counter(step);
counter-increment: step;
width: 1rem;
margin-right: 1.5rem;
display: inline-block;
text-align: right;
color: rgba(115,138,148,.6)
.shiki
.tab, .space
position: relative
.tab::before
content: '⇥';
position: absolute;
opacity: 0.3;
.space::before
content: '·';
position: absolute;
opacity: 0.3;
.sample > .toolbar .sample > .toolbar
right: 0.9375em !important right: 0.9375em !important
top: 3.7875em !important top: 3.7875em !important
@@ -50,12 +80,17 @@ code
font-family: $primary-font-family font-family: $primary-font-family
pointer-events: all pointer-events: all
user-select: none user-select: none
transition: opacity .3s ease-in-out
z-index: 10
.toolbar-item .toolbar-item
a a
font-size: large !important font-size: large !important
border-radius: 0px !important border-radius: 0px !important
background: none !important background: none !important
box-shadow: 0 2px #0003
padding: 0 .5em
color: #bbb
.token.tab:not(:empty), .token.tab:not(:empty),
.token.cr, .token.cr,
@@ -87,7 +122,7 @@ code
content: '\00B7' content: '\00B7'
@media print { @media print {
.code-toolbar>.toolbar { pre .toolbar {
display: none !important; display: none !important;
} }
} }
@@ -2,18 +2,10 @@ import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page'; import { AutoloadPage } from 'vj/misc/Page';
import { i18n, tpl } from 'vj/utils'; import { i18n, tpl } from 'vj/utils';
const highlighterPage = new AutoloadPage('highlighterPage', () => { export default new AutoloadPage('highlighterPage', () => {
import('./prismjs').then(({ default: prismjs }) => { import('./shiki').then(({ highlightBlocks }) => {
function runHighlight($container) { function runHighlight($container) {
$container.find('pre code').get().forEach((code) => { highlightBlocks($container);
const language = ($(code).attr('class') || '').trim();
const m = language.match(/language-([a-z0-9]+)(\|[\d,-]+)/);
if (m?.[2]) {
$(code).parent().attr('data-line', m[2].substring(1));
$(code).attr('class', `language-${m[1]}`);
}
});
prismjs.highlightBlocks($container);
$container.find('pre code').get().forEach((code) => { $container.find('pre code').get().forEach((code) => {
const $code = $(code); const $code = $(code);
const $root = $code.parent().parent(); const $root = $code.parent().parent();
@@ -41,5 +33,3 @@ const highlighterPage = new AutoloadPage('highlighterPage', () => {
$(document).on('vjContentNew', (e) => runHighlight($(e.target))); $(document).on('vjContentNew', (e) => runHighlight($(e.target)));
}); });
}); });
export default highlighterPage;
@@ -1,122 +0,0 @@
import Prism from 'prismjs'; // eslint-disable-line
import 'prismjs/plugins/toolbar/prism-toolbar';
import 'prismjs/plugins/toolbar/prism-toolbar.css';
import 'prismjs/plugins/line-numbers/prism-line-numbers';
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
import 'prismjs/plugins/line-highlight/prism-line-highlight';
import Clipboard from 'clipboard';
import $ from 'jquery';
import components from 'prismjs/components';
import getLoader from 'prismjs/dependencies';
import Notification from 'vj/components/notification/index';
import { i18n } from 'vj/utils';
import languageMeta from './meta';
const files = require.context('prismjs/components/', true, /prism-[a-z0-9-]+\.js/);
const loadedLanguages = new Set();
function loadLanguages() {
const languages = Object.keys(components.languages).filter((l) => l !== 'meta');
const loaded = [...loadedLanguages, ...Object.keys(Prism.languages)];
getLoader(components, languages, loaded).load((lang) => {
files(`./prism-${lang}.js`);
loadedLanguages.add(lang);
});
}
const languageExtMap = {};
loadLanguages();
// Map possible language names to Prism language name
languageMeta.forEach((meta) => {
for (let i = 0; i < meta.ext.length; ++i) {
if (Prism.languages[meta.ext[i]] !== undefined) {
meta.target = meta.ext[i];
break;
}
}
meta.ext.forEach((ext) => {
languageExtMap[ext] = meta.target;
});
});
// Copy to Clipboard
Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
const linkCopy = document.createElement('a');
linkCopy.href = 'javascript:;';
linkCopy.textContent = 'Copy';
const clip = new Clipboard(linkCopy, { text: () => env.code });
clip.on('success', () => {
Notification.success(i18n('Content copied to clipboard!'), 1000);
});
clip.on('error', () => {
Notification.error(i18n('Copy failed :('));
});
return linkCopy;
});
const invisibles = {
tab: /\t/,
crlf: /\r\n/,
lf: /\n/,
cr: /\r/,
space: / /,
};
function addInvisibles(grammar) {
if (!grammar || grammar.tab) return;
for (const name in invisibles) {
if (Object.prototype.hasOwnProperty.call(invisibles, name)) {
grammar[name] = invisibles[name];
}
}
for (const name in grammar) {
if (Object.prototype.hasOwnProperty.call(grammar, name) && !invisibles[name]) {
if (name === 'rest') addInvisibles(grammar.rest);
else handlerInvisiblesToken(grammar, name); // eslint-disable-line @typescript-eslint/no-use-before-define
}
}
}
function handlerInvisiblesToken(tokens, name) {
const value = tokens[name];
const type = Prism.util.type(value);
if (type === 'RegExp') {
const inside = {};
tokens[name] = { pattern: value, inside };
addInvisibles(inside);
} else if (type === 'Array') {
for (let i = 0, l = value.length; i < l; i++) handlerInvisiblesToken(value, i);
} else {
value.inside ||= {};
addInvisibles(value.inside);
}
}
Prism.hooks.add('before-highlight', (env) => {
if (UserContext.showInvisibleChar) addInvisibles(env.grammar);
});
const prismjsApiWrap = {
highlightBlocks: ($dom) => {
$dom.find('pre code').get().forEach((code) => {
const $code = $(code);
const $pre = $code.parent();
$pre.addClass('syntax-hl');
const language = ($(code).attr('class') || '').trim();
// try to map the language name
const m = language.match(/language-([a-z]+)/);
if (m && m[1]) {
const languageName = m[1].toLowerCase();
if (languageExtMap[languageName]) {
$(code).attr('class', `language-${languageExtMap[languageName]}`);
}
}
Prism.highlightElement(code);
});
},
highlight: (text, grammar, language) => Prism.highlight(text, grammar, language),
Prism,
};
export default prismjsApiWrap;
@@ -0,0 +1,110 @@
import {
transformerMetaHighlight, transformerNotationDiff, transformerNotationHighlight,
transformerNotationWordHighlight, transformerRenderWhitespace,
} from '@shikijs/transformers';
import $ from 'jquery';
import {
bundledLanguages,
bundledLanguagesAlias,
createHighlighter,
} from 'shiki/bundle/web';
import Notification from 'vj/components/notification/index';
import { i18n } from 'vj/utils';
$(document).on('click', '[data-clipboard-text]', function (ev) {
const content = $(this).attr('data-clipboard-text');
const $temp = $('<textarea>');
$('body').append($temp);
$temp.val(content).select();
document.execCommand('copy');
$temp.remove();
Notification.success(i18n('Content copied to clipboard!'), 1000);
ev.preventDefault();
});
const transformers = [
transformerNotationDiff(),
transformerNotationHighlight(),
transformerNotationWordHighlight(),
transformerMetaHighlight(),
];
if (UserContext.showInvisibleChar) {
transformers.push(transformerRenderWhitespace());
}
async function highlightBlocks($dom) {
const eles = $dom.find('pre code').get();
const langs = Array.from(new Set(eles.map((code) => {
const language = ($(code).attr('class') || '').trim();
const m = language.match(/language-([a-z0-9]+)/);
return m ? m[1] : null;
}))).filter((i: string) => i && (bundledLanguages[i] || bundledLanguagesAlias[i])) as string[];
const shiki = await createHighlighter({ langs, themes: ['github-dark', 'light-plus'] });
for (const code of eles) {
const $code = $(code);
const $pre = $code.parent();
if ($pre.hasClass('syntax-hl')) continue;
const language = ($(code).attr('class') || '').trim();
console.log(language);
// try to map the language name
const m = language.match(/language-([a-z0-9]+)(?=\.line-numbers)?(?=\|([\d,-]+))?/);
if (!m) continue;
$pre.replaceWith($(shiki.codeToHtml($code.text(), {
lang: m[1],
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' },
],
},
],
},
],
});
},
}),
})));
}
}
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' });
}
export {
highlightBlocks,
highlight,
createHighlighter,
};
+2 -1
View File
@@ -92,7 +92,6 @@
"plugin-error": "^2.0.1", "plugin-error": "^2.0.1",
"postcss-loader": "7.3.4", "postcss-loader": "7.3.4",
"prism-themes": "^1.9.0", "prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qface": "^1.4.1", "qface": "^1.4.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
@@ -131,6 +130,7 @@
"webpackbar": "^6.0.1" "webpackbar": "^6.0.1"
}, },
"dependencies": { "dependencies": {
"@shikijs/transformers": "^1.11.1",
"ansi_up": "^6.0.2", "ansi_up": "^6.0.2",
"cssfilter": "^0.0.10", "cssfilter": "^0.0.10",
"esbuild": "0.23.0", "esbuild": "0.23.0",
@@ -149,6 +149,7 @@
"markdown-it-table-of-contents": "^0.6.0", "markdown-it-table-of-contents": "^0.6.0",
"nunjucks": "^3.2.4", "nunjucks": "^3.2.4",
"schemastery-jsonschema": "^1.1.0", "schemastery-jsonschema": "^1.1.0",
"shiki": "^1.11.1",
"streamsaver": "^2.0.6", "streamsaver": "^2.0.6",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"xss": "^1.0.15" "xss": "^1.0.15"
@@ -76,13 +76,16 @@ const page = new NamedPage('problem_files', () => {
} }
} }
let prism = null; let shiki = null;
const load = import('../components/highlighter/prismjs'); let shikiPromise = null;
load.then(({ default: p }) => {
prism = p.Prism;
}).catch(() => { });
async function handleClickRenameSelected(type) { async function handleClickRenameSelected(type) {
shikiPromise ||= import('../components/highlighter/shiki')
.then(({ createHighlighter }) => createHighlighter({ langs: ['regexp'], themes: ['light-plus'] }))
.then((s) => {
shiki = s;
console.log('shiki loaded');
}).catch(console.error);
const selectedFiles = ensureAndGetSelectedFiles(type); const selectedFiles = ensureAndGetSelectedFiles(type);
if (!selectedFiles?.length) return; if (!selectedFiles?.length) return;
let onActionButton = (_: string) => false; let onActionButton = (_: string) => false;
@@ -113,7 +116,13 @@ const page = new NamedPage('problem_files', () => {
if (copy.endsWith('/')) { if (copy.endsWith('/')) {
copy = copy.substring(0, copy.length - 1); copy = copy.substring(0, copy.length - 1);
flags.reverse(); flags.reverse();
if (prism) setHighlight(`/${prism.highlight(copy, prism.languages.regex, 'RegExp')}/${flags.join('')}`); if (shiki) {
const html = shiki.codeToHtml(copy, {
lang: 'regex',
theme: 'light-plus',
});
setHighlight(`/${$(html).html()}/${flags.join('')}`);
}
try { try {
s = new RegExp(copy, flags.join('')); s = new RegExp(copy, flags.join(''));
setRegexValid(true); setRegexValid(true);
@@ -161,7 +170,7 @@ const page = new NamedPage('problem_files', () => {
const style = { fontFamily: 'var(--code-font-family)' }; const style = { fontFamily: 'var(--code-font-family)' };
return <div className="typo" style={{ maxHeight: '60vh', overflow: 'scroll' }}> return <div className="typo" style={{ maxHeight: '60vh', overflow: 'auto' }}>
{!preview ? <> {!preview ? <>
<div className="row"> <div className="row">
<div className="medium-6 small-6 columns"> <div className="medium-6 small-6 columns">
@@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<div class="section__body"> <div class="section__body">
<pre class="line-numbers"><code class="language-{{ model.setting.langs[rdoc.lang].highlight }}">{{ rdoc['code'] }}</code></pre> <pre><code class="language-{{ model.setting.langs[rdoc.lang].highlight }} line-numbers">{{ rdoc['code'] }}</code></pre>
</div> </div>
</div> </div>
{% endif %} {% endif %}