2026-05-31 13:54:31 +02:00

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 };