104 lines
2.7 KiB
TypeScript
104 lines
2.7 KiB
TypeScript
|
import Prism from 'prismjs'
|
|||
|
import { JSX } from 'solid-js'
|
|||
|
import 'prismjs/components/prism-json'
|
|||
|
import 'prismjs/components/prism-markup'
|
|||
|
import 'prismjs/themes/prism-tomorrow.css'
|
|||
|
|
|||
|
import styles from '../styles/CodePreview.module.css'
|
|||
|
|
|||
|
/**
|
|||
|
* Определяет язык контента (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> {
|
|||
|
content: string
|
|||
|
language?: string
|
|||
|
maxHeight?: string
|
|||
|
}
|
|||
|
|
|||
|
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')
|
|||
|
}
|
|||
|
|
|||
|
return (
|
|||
|
<pre
|
|||
|
{...props}
|
|||
|
class={`${styles.codePreview} ${props.class || ''}`}
|
|||
|
style={`max-height: ${props.maxHeight || '500px'}; overflow-y: auto; ${props.style || ''}`}
|
|||
|
>
|
|||
|
<code
|
|||
|
class={`language-${language()} ${styles.code}`}
|
|||
|
innerHTML={Prism.highlight(numberedCode(), Prism.languages[language()], language())}
|
|||
|
/>
|
|||
|
{props.language && <span class={styles.languageBadge}>{props.language}</span>}
|
|||
|
</pre>
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
export default CodePreview
|
|||
|
export { detectLanguage, formatCode }
|