This commit is contained in:
@@ -1,101 +1,102 @@
|
||||
import Prism from 'prismjs'
|
||||
import { JSX } from 'solid-js'
|
||||
import 'prismjs/components/prism-json'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import { createMemo, JSX, Show } from 'solid-js'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
|
||||
import styles from '../styles/CodePreview.module.css'
|
||||
import { detectLanguage, formatCode, highlightCode } from '../utils/codeHelpers'
|
||||
|
||||
/**
|
||||
* Определяет язык контента (html или json)
|
||||
*/
|
||||
function detectLanguage(content: string): string {
|
||||
try {
|
||||
JSON.parse(content)
|
||||
return 'json'
|
||||
} catch {
|
||||
if (/<[^>]*>/g.test(content)) {
|
||||
return 'markup'
|
||||
}
|
||||
}
|
||||
return 'plaintext'
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует XML/HTML с отступами
|
||||
*/
|
||||
function prettyFormatXML(xml: string): string {
|
||||
let formatted = ''
|
||||
const reg = /(>)(<)(\/*)/g
|
||||
const res = xml.replace(reg, '$1\r\n$2$3')
|
||||
let pad = 0
|
||||
res.split('\r\n').forEach((node) => {
|
||||
let indent = 0
|
||||
if (node.match(/.+<\/\w[^>]*>$/)) {
|
||||
indent = 0
|
||||
} else if (node.match(/^<\//)) {
|
||||
if (pad !== 0) pad -= 2
|
||||
} else if (node.match(/^<\w([^>]*[^/])?>.*$/)) {
|
||||
indent = 2
|
||||
} else {
|
||||
indent = 0
|
||||
}
|
||||
formatted += `${' '.repeat(pad)}${node}\r\n`
|
||||
pad += indent
|
||||
})
|
||||
return formatted.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует и подсвечивает код
|
||||
*/
|
||||
function formatCode(content: string): string {
|
||||
const language = detectLanguage(content)
|
||||
|
||||
if (language === 'json') {
|
||||
try {
|
||||
const formatted = JSON.stringify(JSON.parse(content), null, 2)
|
||||
return Prism.highlight(formatted, Prism.languages[language], language)
|
||||
} catch {
|
||||
return content
|
||||
}
|
||||
} else if (language === 'markup') {
|
||||
const formatted = prettyFormatXML(content)
|
||||
return Prism.highlight(formatted, Prism.languages[language], language)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
interface CodePreviewProps extends JSX.HTMLAttributes<HTMLPreElement> {
|
||||
interface CodePreviewProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
content: string
|
||||
language?: string
|
||||
maxHeight?: string
|
||||
showLineNumbers?: boolean
|
||||
autoFormat?: boolean
|
||||
editable?: boolean
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Компонент для отображения кода с подсветкой синтаксиса
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CodePreview
|
||||
* content='{"key": "value"}'
|
||||
* language="json"
|
||||
* showLineNumbers={true}
|
||||
* editable={true}
|
||||
* onEdit={() => setIsEditing(true)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
const CodePreview = (props: CodePreviewProps) => {
|
||||
const language = () => props.language || detectLanguage(props.content)
|
||||
// const formattedCode = () => formatCode(props.content)
|
||||
|
||||
const numberedCode = () => {
|
||||
const lines = props.content.split('\n')
|
||||
return lines
|
||||
.map((line, index) => `<span class="${styles.lineNumber}">${index + 1}</span>${line}`)
|
||||
.join('\n')
|
||||
}
|
||||
// Реактивные вычисления
|
||||
const language = createMemo(() => props.language || detectLanguage(props.content))
|
||||
const formattedContent = createMemo(() =>
|
||||
props.autoFormat ? formatCode(props.content, language()) : props.content
|
||||
)
|
||||
const highlightedCode = createMemo(() => highlightCode(formattedContent(), language()))
|
||||
const isEmpty = createMemo(() => !props.content?.trim())
|
||||
|
||||
return (
|
||||
<pre
|
||||
{...props}
|
||||
class={`${styles.codePreview} ${props.class || ''}`}
|
||||
style={`max-height: ${props.maxHeight || '500px'}; overflow-y: auto; ${props.style || ''}`}
|
||||
<div
|
||||
class={`${styles.codePreview} ${props.editable ? styles.codePreviewContainer : ''} ${props.class || ''}`}
|
||||
style={`max-height: ${props.maxHeight || '500px'}; ${props.style || ''}`}
|
||||
onClick={props.editable ? props.onEdit : undefined}
|
||||
role={props.editable ? 'button' : 'presentation'}
|
||||
tabindex={props.editable ? 0 : undefined}
|
||||
onKeyDown={(e) => {
|
||||
if (props.editable && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault()
|
||||
props.onEdit?.()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<code
|
||||
class={`language-${language()} ${styles.code}`}
|
||||
innerHTML={Prism.highlight(numberedCode(), Prism.languages[language()], language())}
|
||||
/>
|
||||
{props.language && <span class={styles.languageBadge}>{props.language}</span>}
|
||||
</pre>
|
||||
<div class={styles.codeContainer}>
|
||||
{/* Область кода */}
|
||||
<div class={styles.codeArea}>
|
||||
<Show
|
||||
when={!isEmpty()}
|
||||
fallback={
|
||||
<div class={`${styles.placeholder} ${props.editable ? styles.placeholderClickable : ''}`}>
|
||||
{props.editable ? 'Нажмите для редактирования...' : 'Нет содержимого'}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<pre class={styles.codePreviewContent}>
|
||||
<code class={`language-${language()}`} innerHTML={highlightedCode()} />
|
||||
</pre>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Индикаторы */}
|
||||
<div class={styles.controlsLeft}>
|
||||
<span class={styles.languageBadge}>{language()}</span>
|
||||
|
||||
<Show when={props.editable}>
|
||||
<div class={styles.statusIndicator}>
|
||||
<div class={`${styles.statusDot} ${styles.idle}`} />
|
||||
<span>Только чтение</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Кнопка редактирования */}
|
||||
<Show when={props.editable && !isEmpty()}>
|
||||
<div class={styles.controlsRight}>
|
||||
<button
|
||||
class={styles.editButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onEdit?.()
|
||||
}}
|
||||
title="Редактировать код"
|
||||
>
|
||||
✏️ Редактировать
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user