codice
Version:
Codice is a slim React components suite for code editing and displaying story. It provides an editor component and a code block component with syntax highlighting. Styling customization is enabled through CSS variables and HTML attributes.
286 lines (281 loc) • 8.45 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useMemo, createElement } from 'react';
import { generate, tokenize } from 'sugar-high';
import * as presets from 'sugar-high/presets';
function ScopedStyle({ css }) {
return /*#__PURE__*/ jsx("style", {
"data-codice-style": true,
children: `@scope {\n${css}\n}`
});
}
const fontSizeCss = (fontSize)=>{
const fz = `${fontSize != null ? fontSize : 'inherit'}${typeof fontSize === 'number' ? 'px' : ''}`;
return fz;
};
const C = `:scope[data-codice-code]`;
const H = `:scope[data-codice-header]`;
const L = `:scope[data-codice-line-numbers="true"]`;
const FL = `:scope[data-codice-line-numbers="false"]`;
const BASE_CSS = `\
${C} {
padding: calc(var(--codice-code-padding) / 2) 0;
}
${C} [data-codice-code-content] {
padding: calc(var(--codice-code-padding) * 0.25) 0;
}
${C} pre {
white-space: pre-wrap;
margin: 0;
}
${C} code {
display: block;
border: none;
}
${C} .sh__line {
display: inline-block;
width: 100%;
}
${C} .sh__line[data-highlight] {
background-color: var(--codice-code-highlight-color);
}
`;
const HEADER_CSS = `\
${H} {
position: relative;
display: flex;
padding: calc(var(--codice-code-padding) * 0.25)
var(--codice-code-padding)
calc(var(--codice-code-padding) * 0.25);
align-items: center;
}
${H} [data-codice-title] {
display: inline-block;
flex: 1 0;
text-align: center;
line-height: 1;
background-color: transparent;
outline: none;
border: none;
caret-color: var(--codice-caret-color);
color: var(--codice-title-color);
}
${H} [data-codice-controls] {
display: inline-flex;
align-self: center;
justify-self: start;
align-items: center;
justify-content: center;
}
${H} [data-codice-controls] {
width: 52px;
}
${H}[data-codice-header-controls="true"] [data-codice-title] {
padding-right: 52px;
}
${H} [data-codice-control] {
display: flex;
width: 10px;
height: 10px;
margin: 3px;
border-radius: 50%;
background-color: var(--codice-control-color);
}
`;
const LINE_NUMBER_CSS = `\
${L} code {
counter-reset: codice-code-line-number;
}
${L} .sh__line:has(> [data-codice-code-line-number]) {
padding-left: var(--codice-code-line-number-width);
}
${L} [data-codice-code-line-number] {
counter-increment: codice-code-line-number 1;
content: counter(codice-code-line-number);
display: inline-block;
min-width: calc(2rem - 6px);
margin-left: calc(var(--codice-code-line-number-width) * -1);
margin-right: 16px;
text-align: right;
user-select: none;
color: var(--codice-code-line-number-color);
}
${FL} .sh__line {
padding-left: var(--codice-code-padding);
}
`;
const CODE_CSS = `${BASE_CSS}\n${HEADER_CSS}\n${LINE_NUMBER_CSS}`;
const css = ({ fontSize, lineNumbersWidth = '2.5rem', padding = '1rem' })=>{
const fz = fontSizeCss(fontSize);
return `\
${CODE_CSS}
${C} {
--codice-font-size: ${fz};
--codice-code-line-number-width: ${lineNumbersWidth};
--codice-code-padding: ${padding};
}
`;
};
const getPresetByExt = (ext)=>{
switch(ext){
case 'sass':
case 'scss':
case 'less':
case 'css':
return presets.css;
case 'py':
return presets.python;
case 'rs':
return presets.rust;
default:
return undefined;
}
};
/** utils */ function getExtension(title) {
return (title || '').split('.').pop() || '';
}
function generateHighlightedLines(codeText, highlightLines, lineNumbers, title, extension) {
const ext = extension || getExtension(title);
const preset = getPresetByExt(ext);
const childrenLines = generate(tokenize(codeText, preset));
// each line will contain class name 'sh__line',
// if it's highlighted, it will contain [data-highlight]
const highlightedLines = new Set();
if (highlightLines) {
for (const line of highlightLines){
if (Array.isArray(line)) {
// Add range of lines
for(let i = line[0]; i <= line[1]; i++){
highlightedLines.add(i);
}
} else {
// Add single line
highlightedLines.add(line);
}
}
}
const lines = childrenLines.map((line, index)=>{
const isHighlighted = highlightedLines.has(index + 1);
const { tagName: Line, properties: lineProperties } = line;
const tokens = line.children.map((child, childIndex)=>{
const { tagName: Token, type, children, properties } = child;
return /*#__PURE__*/ jsx(Token, {
"data-sh-token-type": type,
...properties,
children: children[0].value
}, childIndex);
});
return /*#__PURE__*/ createElement(Line, {
...lineProperties,
...isHighlighted ? {
'data-highlight': true
} : {},
"data-codice-code-line": true,
key: index,
children: [
lineNumbers ? /*#__PURE__*/ jsx("span", {
"data-codice-code-line-number": true,
children: index + 1
}, "ln") : null,
tokens
]
});
});
return lines;
}
function TitleInput({ title, onChange }) {
return /*#__PURE__*/ jsx("input", {
"data-codice-title": true,
value: title,
readOnly: !onChange,
...onChange && {
onChange: (e)=>{
onChange(e.target.value);
}
}
});
}
function CodeHeader({ title, controls = false, onChangeTitle }) {
if (!title && !controls) return null;
return /*#__PURE__*/ jsxs("div", {
"data-codice-header": true,
"data-codice-header-controls": controls,
children: [
/*#__PURE__*/ jsx(ScopedStyle, {
css: HEADER_CSS
}),
controls ? /*#__PURE__*/ jsxs("div", {
"data-codice-controls": true,
children: [
/*#__PURE__*/ jsx("span", {
"data-codice-control": true
}),
/*#__PURE__*/ jsx("span", {
"data-codice-control": true
}),
/*#__PURE__*/ jsx("span", {
"data-codice-control": true
})
]
}) : null,
/*#__PURE__*/ jsx(TitleInput, {
title: title,
onChange: onChangeTitle
})
]
});
}
function CodeFrame({ children, preformatted, asMarkup }) {
const props = asMarkup ? {
dangerouslySetInnerHTML: {
__html: children
}
} : {
children
};
return preformatted ? /*#__PURE__*/ jsx("pre", {
"data-codice-code-content": true,
children: /*#__PURE__*/ jsx("code", {
...props
})
}) : /*#__PURE__*/ jsx("div", {
...props,
"data-codice-code-content": true
});
}
function Code({ children: code, title, controls, fontSize, highlightLines, preformatted = true, lineNumbers = false, lineNumbersWidth, padding, asMarkup = false, extension, ...props }) {
const lineElements = useMemo(()=>asMarkup ? code : generateHighlightedLines(code, highlightLines, lineNumbers, title, extension), [
code,
highlightLines,
lineNumbers,
title,
extension,
asMarkup
]);
return(// Add both attribute because it's both root component and child component (of editor)
/*#__PURE__*/ jsxs("div", {
...props,
"data-codice": "code",
"data-codice-code": true,
"data-codice-line-numbers": lineNumbers,
children: [
/*#__PURE__*/ jsx(ScopedStyle, {
css: css({
fontSize,
lineNumbersWidth,
padding
})
}),
/*#__PURE__*/ jsx(CodeHeader, {
title: title,
controls: controls
}),
/*#__PURE__*/ jsx(CodeFrame, {
preformatted: preformatted,
asMarkup: asMarkup,
children: lineElements
})
]
}));
}
export { CodeHeader as C, ScopedStyle as S, Code as a, fontSizeCss as f, getExtension as g };