133 lines
4.1 KiB
JavaScript
133 lines
4.1 KiB
JavaScript
|
|
import * as React from 'react';
|
||
|
|
import { codeToKeyedTokens, createMagicMoveMachine, syncTokenKeys, toKeyedTokens } from './core.mjs';
|
||
|
|
import { MagicMoveRenderer } from './renderer.mjs';
|
||
|
|
import 'diff-match-patch-es';
|
||
|
|
import 'ohash';
|
||
|
|
|
||
|
|
function normalizeCSSProperties(css) {
|
||
|
|
if (typeof css === 'string') {
|
||
|
|
const style = {};
|
||
|
|
css?.split(';').forEach(pair => {
|
||
|
|
const [key, value] = pair.split(':');
|
||
|
|
if (key && value) style[key.trim()] = value.trim();
|
||
|
|
});
|
||
|
|
return style;
|
||
|
|
}
|
||
|
|
return css;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A wrapper component to `MagicMoveRenderer`
|
||
|
|
*/
|
||
|
|
function ShikiMagicMoveRenderer({
|
||
|
|
animate = true,
|
||
|
|
tokens,
|
||
|
|
previous,
|
||
|
|
options,
|
||
|
|
onStart,
|
||
|
|
onEnd,
|
||
|
|
className,
|
||
|
|
style
|
||
|
|
}) {
|
||
|
|
const container = React.useRef(null);
|
||
|
|
const renderer = React.useRef(null);
|
||
|
|
const [isMounted, setIsMounted] = React.useState(false);
|
||
|
|
React.useEffect(() => {
|
||
|
|
if (!container.current) return;
|
||
|
|
// Remove previous content
|
||
|
|
container.current.innerHTML = '';
|
||
|
|
setIsMounted(true);
|
||
|
|
renderer.current = new MagicMoveRenderer(container.current);
|
||
|
|
}, []);
|
||
|
|
React.useEffect(() => {
|
||
|
|
async function render() {
|
||
|
|
if (!renderer.current) return;
|
||
|
|
Object.assign(renderer.current.options, options);
|
||
|
|
if (animate) {
|
||
|
|
if (previous) renderer.current.replace(previous);
|
||
|
|
onStart?.();
|
||
|
|
await renderer.current.render(tokens);
|
||
|
|
onEnd?.();
|
||
|
|
} else {
|
||
|
|
renderer.current.replace(tokens);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
// FIXME: we should only re-render when tokens change, but react-hooks rule doesn't allow.
|
||
|
|
// Try to correct the dependency array if something goes wrong.
|
||
|
|
}, [tokens]);
|
||
|
|
return /*#__PURE__*/React.createElement("pre", {
|
||
|
|
ref: container,
|
||
|
|
className: `shiki-magic-move-container ${className || ''}`.trim(),
|
||
|
|
style: style
|
||
|
|
}, /*#__PURE__*/React.createElement("div", null, isMounted ? undefined : tokens.tokens.map(token => {
|
||
|
|
if (token.content === '\n') return /*#__PURE__*/React.createElement("br", {
|
||
|
|
key: token.key
|
||
|
|
});
|
||
|
|
return /*#__PURE__*/React.createElement("span", {
|
||
|
|
style: {
|
||
|
|
...normalizeCSSProperties(token.htmlStyle),
|
||
|
|
color: token.color
|
||
|
|
},
|
||
|
|
className: ['shiki-magic-move-item', token.htmlClass].filter(Boolean).join(' '),
|
||
|
|
key: token.key
|
||
|
|
}, token.content);
|
||
|
|
})));
|
||
|
|
}
|
||
|
|
|
||
|
|
function ShikiMagicMove(props) {
|
||
|
|
const codeToTokens = React.useRef((code, lineNumbers) => codeToKeyedTokens(props.highlighter, code, {
|
||
|
|
lang: props.lang,
|
||
|
|
theme: props.theme
|
||
|
|
}, lineNumbers));
|
||
|
|
const machine = React.useRef(null);
|
||
|
|
machine.current = machine.current || createMagicMoveMachine((code, lineNumbers) => codeToTokens.current(code, lineNumbers));
|
||
|
|
const lineNumbers = props.options?.lineNumbers ?? false;
|
||
|
|
const result = React.useMemo(() => {
|
||
|
|
if (props.code === machine.current.current.code && props.theme === machine.current.current.themeName && props.lang === machine.current.current.lang && lineNumbers === machine.current.current.lineNumbers) {
|
||
|
|
return machine.current;
|
||
|
|
}
|
||
|
|
return machine.current.commit(props.code, props.options);
|
||
|
|
}, [props.code, props.options, props.theme, props.lang, lineNumbers]);
|
||
|
|
return /*#__PURE__*/React.createElement(ShikiMagicMoveRenderer, {
|
||
|
|
tokens: result.current,
|
||
|
|
options: props.options,
|
||
|
|
previous: result.previous,
|
||
|
|
onStart: props.onStart,
|
||
|
|
onEnd: props.onEnd,
|
||
|
|
className: props.className
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const EMPTY = /* @__PURE__ */toKeyedTokens('', []);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Component to render a compiled magic move step,
|
||
|
|
* Where the tokens can be generated on build time.
|
||
|
|
*/
|
||
|
|
function ShikiMagicMovePrecompiled({
|
||
|
|
steps,
|
||
|
|
step = 0,
|
||
|
|
animate = true,
|
||
|
|
options,
|
||
|
|
onStart,
|
||
|
|
onEnd
|
||
|
|
}) {
|
||
|
|
const [previous, setPrevious] = React.useState(EMPTY);
|
||
|
|
const result = React.useMemo(() => {
|
||
|
|
const res = syncTokenKeys(previous, steps[Math.min(step, steps.length - 1)], options);
|
||
|
|
setPrevious(res.to);
|
||
|
|
return res;
|
||
|
|
}, [previous, steps, step, options]);
|
||
|
|
return /*#__PURE__*/React.createElement(ShikiMagicMoveRenderer, {
|
||
|
|
tokens: result.to,
|
||
|
|
previous: result.from,
|
||
|
|
options: options,
|
||
|
|
animate: animate,
|
||
|
|
onStart: onStart,
|
||
|
|
onEnd: onEnd
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export { ShikiMagicMove, ShikiMagicMovePrecompiled, ShikiMagicMoveRenderer };
|