mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
ui: switch to shiki
This commit is contained in:
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,
|
||||||
|
};
|
||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in new issue
Block a user