@@ -29,18 +28,30 @@ function CodeExampleItem({
export function CodeExamples() {
return (
-
+
{CODE_ULTIMATE_SNIPPET_HTML}
-
-
- {highlight(`\
+
+
+ {`\
+import { highlight } from 'sugar-high'
+
function marker() {
- return "long live sugar-high"
-}`)}
+ const code = "return 'long live sugar-high'"
+ return highlight(code)
+}
+
+const html = marker()
+
+render(html)
+`}
diff --git a/site/app/editor-example.tsx b/site/app/editor-example.tsx
index 92dea26..64229b6 100644
--- a/site/app/editor-example.tsx
+++ b/site/app/editor-example.tsx
@@ -16,7 +16,6 @@ export default function Page() {
value={code}
className='editor'
title='index.js'
- highlight={text => highlight(text)}
onChange={(text) => setCode(text)}
/>
diff --git a/site/app/live-editor.tsx b/site/app/live-editor.tsx
index b64e99c..d142428 100644
--- a/site/app/live-editor.tsx
+++ b/site/app/live-editor.tsx
@@ -2,7 +2,6 @@
import { Editor } from 'codice'
import { useState } from 'react'
-import { highlight } from 'sugar-high'
const CODE_QUERY_KEY = 'c'
@@ -71,7 +70,6 @@ export function LiveEditor({
title={title}
controls={controls}
lineNumbers={lineNumbers}
- highlight={(text) => highlight(text)}
onChange={(text) => setCode(text)}
/>
diff --git a/site/app/styles.css b/site/app/styles.css
index e001558..eba53a4 100644
--- a/site/app/styles.css
+++ b/site/app/styles.css
@@ -153,16 +153,10 @@ input[type=radio] {
.editor[data-codice-editor] textarea:focus {
border: 1px solid hsla(137, 100.00%, 94.30%, 0.30);
}
-.editor[data-codice-editor-line-numbers="false"] [data-codice-editor-content] {
- padding-left: 0;
-}
-.editor[data-codice-editor-line-numbers="true"] [data-codice-editor-content] {
- padding-left: 16px;
-}
[data-codice-editor-title] {
color: hsla(0, 0%, 87%, 0.34);
}
-:nth
+
[data-codice-editor-control] {
transition: background-color 0.2s ease-in-out;
}
diff --git a/site/next.config.js b/site/next.config.js
new file mode 100644
index 0000000..b275330
--- /dev/null
+++ b/site/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+ experimental: {
+ reactOwnerStack: true,
+ },
+}
diff --git a/src/code/code.test.tsx b/src/code/code.test.tsx
index f613cb1..9dcacc6 100644
--- a/src/code/code.test.tsx
+++ b/src/code/code.test.tsx
@@ -6,7 +6,8 @@ describe('Code', () => {
it('default props', () => {
expect(renderToString(test
)).toMatchInlineSnapshot(`
"test
"
+ [data-codice-code] .sh__line {
+ display: inline-block;
+ width: 100%;
+ }
+ [data-codice-code] .sh__line[data-highlight] {
+ background-color: var(--codice-code-highlight-color);
+ }
+ test
"
`)
})
it('with title', () => {
expect(renderToString(test
)).toMatchInlineSnapshot(`
"file.jstest
"
+ file.jstest
"
`)
})
it('with controls', () => {
expect(renderToString(test
)).toMatchInlineSnapshot(`
"test
"
+ test
"
`)
})
})
\ No newline at end of file
diff --git a/src/code/code.tsx b/src/code/code.tsx
index 539ea50..8610fcb 100644
--- a/src/code/code.tsx
+++ b/src/code/code.tsx
@@ -1,4 +1,66 @@
+import { tokenize, generate } from 'sugar-high'
import { baseCss, headerCss, lineNumbersCss } from './css'
+import { useMemo } from 'react'
+
+function generateHighlightedLines(
+ codeText: string,
+ highlightLines: ([number, number] | number)[],
+ lineNumbers: boolean
+) {
+ const childrenLines = generate(tokenize(codeText))
+
+ // 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, children, properties } = child
+ return (
+
+ {(children[0].value)}
+
+ )
+ })
+
+
+ return (
+
+ {lineNumbers ? {index + 1} : null}
+ {tokens}
+
+ )
+ })
+ )
+ return lines
+}
export function CodeHeader({ title, controls = false }: { title?: string; controls: boolean }) {
if (!title && !controls) return null
@@ -25,6 +87,7 @@ export function Code({
controls,
preformatted = true,
lineNumbers = false,
+ highlightLines,
...props
}: {
children: string
@@ -33,8 +96,13 @@ export function Code({
title?: string
controls?: boolean
lineNumbers?: boolean
+ highlightLines?: ([number, number] | number)[]
} & React.HTMLAttributes) {
const css = baseCss + (lineNumbers ? lineNumbersCss : '')
+ const lineElements = useMemo(() =>
+ generateHighlightedLines(code, highlightLines, lineNumbers),
+ [code, highlightLines, lineNumbers]
+ )
return (
@@ -44,10 +112,14 @@ export function Code({
{preformatted ? (
-
+
+ <>
+ {lineElements}
+ >
+
) : (
- {code}
+ {lineElements}
)}
)
diff --git a/src/code/css.ts b/src/code/css.ts
index 81be509..f2ecf1d 100644
--- a/src/code/css.ts
+++ b/src/code/css.ts
@@ -3,7 +3,8 @@ const H = `[data-codice-editor-header]`
export const baseCss = `\
${C} {
- --codice-editor-line-number-color: #a4a4a4;
+ --codice-code-line-number-color: #a4a4a4;
+ --codice-code-highlight-color: #555555;
}
${C} pre {
white-space: pre-wrap;
@@ -12,6 +13,13 @@ ${C} pre {
${C} code {
border: none;
}
+${C} .sh__line {
+ display: inline-block;
+ width: 100%;
+}
+${C} .sh__line[data-highlight] {
+ background-color: var(--codice-code-highlight-color);
+}
`
export const headerCss = `\
@@ -50,16 +58,19 @@ ${H} [data-codice-editor-control] {
export const lineNumbersCss = `\
@scope {
- code { counter-reset: codice-code-line-number; }
- .sh__line::before {
+ code {
+ counter-reset: codice-code-line-number;
+ padding-left
+ }
+ [data-codice-code-line-number] {
counter-increment: codice-code-line-number 1;
content: counter(codice-code-line-number);
display: inline-block;
min-width: 24px;
- margin-right: 18px;
- margin-left: -42px;
+ margin-right: 16px;
text-align: right;
- color: var(--codice-editor-line-number-color);
+ user-select: none;
+ color: var(--codice-code-line-number-color);
}
}
`
\ No newline at end of file
diff --git a/src/editor/css.ts b/src/editor/css.ts
index 5bf76e9..42502c3 100644
--- a/src/editor/css.ts
+++ b/src/editor/css.ts
@@ -14,7 +14,7 @@ ${R} textarea {
line-break: anywhere;
overflow-wrap: break-word;
scrollbar-width: none;
- padding: 24px 36px;
+ padding: 24px 16px;
font-size: 16px;
line-height: 20px;
caret-color: var(--codice-editor-caret-color);
@@ -48,6 +48,7 @@ ${R} textarea {
overflow: hidden;
}
${R}[data-codice-editor-line-numbers="true"] textarea {
- padding-left: 51px;
+ padding-left: 55px;
}
-`
\ No newline at end of file
+`
+// line number padding-left is [[width 24px] margin-right 16px] + 15px
\ No newline at end of file
diff --git a/src/editor/editor.test.tsx b/src/editor/editor.test.tsx
index ae3f407..36159fb 100644
--- a/src/editor/editor.test.tsx
+++ b/src/editor/editor.test.tsx
@@ -23,7 +23,7 @@ describe('Code', () => {
line-break: anywhere;
overflow-wrap: break-word;
scrollbar-width: none;
- padding: 24px 36px;
+ padding: 24px 16px;
font-size: 16px;
line-height: 20px;
caret-color: var(--codice-editor-caret-color);
@@ -57,7 +57,7 @@ describe('Code', () => {
overflow: hidden;
}
[data-codice-editor][data-codice-editor-line-numbers="true"] textarea {
- padding-left: 51px;
+ padding-left: 55px;
}
"
@@ -137,7 +148,7 @@ describe('Code', () => {
line-break: anywhere;
overflow-wrap: break-word;
scrollbar-width: none;
- padding: 24px 36px;
+ padding: 24px 16px;
font-size: 16px;
line-height: 20px;
caret-color: var(--codice-editor-caret-color);
@@ -171,7 +182,7 @@ describe('Code', () => {
overflow: hidden;
}
[data-codice-editor][data-codice-editor-line-numbers="true"] textarea {
- padding-left: 51px;
+ padding-left: 55px;
}
file.js
"
@@ -251,7 +273,7 @@ describe('Code', () => {
line-break: anywhere;
overflow-wrap: break-word;
scrollbar-width: none;
- padding: 24px 36px;
+ padding: 24px 16px;
font-size: 16px;
line-height: 20px;
caret-color: var(--codice-editor-caret-color);
@@ -285,10 +307,11 @@ describe('Code', () => {
overflow: hidden;
}
[data-codice-editor][data-codice-editor-line-numbers="true"] textarea {
- padding-left: 51px;
+ padding-left: 55px;
}
"
diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx
index 1761d54..d4c05b8 100644
--- a/src/editor/editor.tsx
+++ b/src/editor/editor.tsx
@@ -4,13 +4,17 @@ import { useEffect, useState, useRef, forwardRef } from 'react'
import { Code } from '../code'
import { CodeHeader } from '../code/code'
-function composeRefs(...refs) {
- return (node) => {
+function composeRefs(...refs: React.Ref[]) {
+ return (node: HTMLElement | null) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
- ref(node)
+ if (node) {
+ ref(node)
+ }
} else if (ref) {
- ref.current = node
+ if (node) {
+ ref.current = node
+ }
}
})
}
@@ -23,35 +27,32 @@ const Editor = forwardRef(function EditorComponent(
controls,
lineNumbers,
onChange = () => {},
- highlight = () => '',
}: {
title?: string
value?: string
controls?: boolean
lineNumbers?: boolean
onChange?: (code: string) => void
- highlight?: (code: string) => string
} & React.HTMLAttributes,
ref: React.Ref
) {
- const [text, setText] = useState(value)
- const [output, setOutput] = useState(() => highlight(text))
+ const [code, setCode] = useState(value)
const textareaRef = useRef(null)
- function update(code: string) {
- const highlighted = highlight(code)
- setText(code)
- setOutput(highlighted)
- onChange(code)
+ function update(textContent: string) {
+ setCode(textContent)
+ onChange(textContent)
}
useEffect(() => {
- update(value)
- }, [value])
+ if (value !== code) {
+ update(value)
+ }
+ }, [value, code])
- function onInput(event) {
- const code = event.target.value || ''
- update(code)
+ function onInput(event: React.ChangeEvent) {
+ const textContent = event.target.value || ''
+ update(textContent)
}
return (
@@ -65,9 +66,9 @@ const Editor = forwardRef(function EditorComponent(
controls={false}
lineNumbers={lineNumbers ?? true}
>
- {output}
+ {code}
-
+
>
)
diff --git a/src/editor/index.tsx b/src/editor/index.tsx
index b830e57..5f5dd25 100644
--- a/src/editor/index.tsx
+++ b/src/editor/index.tsx
@@ -4,8 +4,8 @@ import { css } from './css'
export type EditorProps = React.HTMLAttributes