forked from defunkt/howl
/ |/ \ / \ / \ / | $$$$$$$$//$$$$$$ |$$$$$$$ |/$$$$$$ |$$$$$$$$/ $$ |__ $$ | $$ |$$ |__$$ |$$ | _$$/ $$ |__ $$ | $$ | $$ |$$ $$< $$ |/ |$$ | $$$$$/ $$ | $$ |$$$$$$$ |$$ |$$$$ |$$$$$/ $$ | $$ \__$$ |$$ | $$ |$$ \__$$ |$$ |_____ $$ | $$ $$/ $$ | $$ |$$ $$/ $$ | $$/ $$$$$$/ $$/ $$/ $$$$$$/ $$$$$$$$/
233 lines
5.5 KiB
TypeScript
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
|
|
}
|