UNPKG

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