howl/src/code.tsx
Chris Wanstrath 0cad100197 ________ ______ _______ ______ ________
/        |/      \ /       \  /      \ /        |
$$$$$$$$//$$$$$$  |$$$$$$$  |/$$$$$$  |$$$$$$$$/
$$ |__   $$ |  $$ |$$ |__$$ |$$ | _$$/ $$ |__
$$    |  $$ |  $$ |$$    $$< $$ |/    |$$    |
$$$$$/   $$ |  $$ |$$$$$$$  |$$ |$$$$ |$$$$$/
$$ |     $$ \__$$ |$$ |  $$ |$$ \__$$ |$$ |_____
$$ |     $$    $$/ $$ |  $$ |$$    $$/ $$       |
$$/       $$$$$$/  $$/   $$/  $$$$$$/  $$$$$$$$/
2026-01-16 08:33:38 -08:00

233 lines
5.5 KiB
TypeScript

import { define } from 'forge'
import { theme } from './theme'
// Code block container
const CodeBlock = define('CodeBlock', {
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: 1.5,
color: theme('syntax-text'),
})
// Syntax token spans
const TagToken = define('TagToken', {
base: 'span',
color: theme('syntax-tag'),
fontWeight: 600,
})
const AttrToken = define('AttrToken', {
base: 'span',
color: theme('syntax-attr'),
})
const StringToken = define('StringToken', {
base: 'span',
color: theme('syntax-string'),
})
const NumberToken = define('NumberToken', {
base: 'span',
color: theme('syntax-number'),
})
const BraceToken = define('BraceToken', {
base: 'span',
color: theme('syntax-brace'),
fontWeight: 600,
})
const TextToken = define('TextToken', {
base: 'span',
color: theme('syntax-text'),
})
// Code examples container
const CodeExamplesBox = define('CodeExamplesBox', {
background: theme('colors-bgElevated'),
padding: theme('spacing-4'),
borderRadius: theme('radius-lg'),
border: `1px solid ${theme('colors-border')}`,
display: 'flex',
flexDirection: 'column',
gap: theme('spacing-2'),
})
type CodeProps = {
children: string
}
// Lightweight JSX syntax highlighter
export const Code = ({ children }: CodeProps) => {
const tokens = tokenizeJSX(children)
return (
<CodeBlock>
{tokens.map((token, i) => {
if (token.type === 'tag') {
return <TagToken key={i}>{token.value}</TagToken>
}
if (token.type === 'attr') {
return <AttrToken key={i}>{token.value}</AttrToken>
}
if (token.type === 'string') {
return <StringToken key={i}>{token.value}</StringToken>
}
if (token.type === 'number') {
return <NumberToken key={i}>{token.value}</NumberToken>
}
if (token.type === 'brace') {
return <BraceToken key={i}>{token.value}</BraceToken>
}
return <TextToken key={i}>{token.value}</TextToken>
})}
</CodeBlock>
)
}
type CodeExamplesProps = {
examples: string[]
}
// Container for multiple code examples
export const CodeExamples = ({ examples }: CodeExamplesProps) => {
return (
<CodeExamplesBox class="code-examples-container">
{examples.map((example, i) => (
<Code key={i}>{example}</Code>
))}
</CodeExamplesBox>
)
}
type Token = {
type: 'tag' | 'attr' | 'string' | 'number' | 'brace' | 'text'
value: string
}
function tokenizeJSX(code: string): Token[] {
const tokens: Token[] = []
let i = 0
while (i < code.length) {
// Match opening/closing tags: < or </
if (code[i] === '<') {
i++
// Check for closing tag
if (code[i] === '/') {
tokens.push({ type: 'tag', value: '</' })
i++
} else {
tokens.push({ type: 'tag', value: '<' })
}
// Get tag name
let tagName = ''
while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) {
tagName += code[i]
i++
}
if (tagName) {
tokens.push({ type: 'tag', value: tagName })
}
// Parse attributes inside the tag
while (i < code.length && code[i] !== '>') {
// Skip whitespace
if (/\s/.test(code[i]!)) {
tokens.push({ type: 'text', value: code[i]! })
i++
continue
}
// Check for self-closing /
if (code[i] === '/' && code[i + 1] === '>') {
tokens.push({ type: 'tag', value: ' />' })
i += 2
break
}
// Parse attribute name
let attrName = ''
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) {
attrName += code[i]
i++
}
if (attrName) {
tokens.push({ type: 'attr', value: attrName })
}
// Check for =
if (code[i] === '=') {
tokens.push({ type: 'text', value: '=' })
i++
// Parse attribute value
if (code[i] === '"') {
// String value
let str = '"'
i++
while (i < code.length && code[i] !== '"') {
str += code[i]
i++
}
if (code[i] === '"') {
str += '"'
i++
}
tokens.push({ type: 'string', value: str })
} else if (code[i] === '{') {
// Brace value
tokens.push({ type: 'brace', value: '{' })
i++
// Get content inside braces
let content = ''
let depth = 1
while (i < code.length && depth > 0) {
if (code[i] === '{') depth++
if (code[i] === '}') {
depth--
if (depth === 0) break
}
content += code[i]
i++
}
// Check if content is a number
if (/^\d+$/.test(content)) {
tokens.push({ type: 'number', value: content })
} else {
tokens.push({ type: 'text', value: content })
}
if (code[i] === '}') {
tokens.push({ type: 'brace', value: '}' })
i++
}
}
}
}
// Closing >
if (code[i] === '>') {
tokens.push({ type: 'tag', value: '>' })
i++
}
} else {
// Regular text
let text = ''
while (i < code.length && code[i] !== '<') {
text += code[i]
i++
}
if (text) {
tokens.push({ type: 'text', value: text })
}
}
}
return tokens
}