/* eslint-disable no-irregular-whitespace, no-useless-escape */ /* global ComponentManager */ /** *Syntaxhighlightercomponent -Component for Syntax Highlighting *The usage format is consistent with Apicomponent for consistent use. * *feature: *-Automatic language detection *-Supports multiple languages ​​(HTML, CSS, Javascript, PHP, Bash) *-Supports the theme (Light/Dark) *-Automatic code formatting *-Show line number *-There is a copy button. *-Code Folding *-Supports multiple languages ​​(i18n) */ const SyntaxHighlighterComponent = { config: { autoDetect: true, // Automatic language detection language: null, // Language that needs highlights such as 'JavaScript', 'PHP', 'HTML' lineNumbers: true, // Show the line number copyButton: true, // Show the copy button highlightLines: true, // Emphasize the selected line color autoIndent: true, // Automatic code formatting themeName: 'light', // The theme 'Light' or 'Dark' theme codeFolding: false, // Code folding loadingText: 'Loading', errorText: 'Error', copyText: 'Code', copiedText: 'Copied!', languages: ['html', 'css', 'javascript', 'php', 'bash', 'json', 'xml', 'sql', 'python', 'ruby'], languagePatterns: { html: /^/gi, attribute: /\s+([a-z0-9-]+)(?:=(?:["'](?:\\.|[^\\"])*["']|[^\s"'>]+))?/gi, string: /"[^"]*"|'[^']*'/g, comment: //g, entity: /&[a-z0-9#]+;/gi }, css: { selector: /[.#][a-z0-9-_:]+|[a-z0-9-]+(?=\s*\{)/gi, property: /[a-z-]+(?=\s*:)/gi, value: /:\s*[^;]+/g, unit: /\d+(?:px|em|rem|vh|vw|%)/gi, color: /#[a-f0-9]{3,8}|rgba?\([^)]+\)/gi, comment: /\/\*[\s\S]*?\*\//g, punctuation: /[{};:]/g }, javascript: { keyword: /\b(?:const|let|var|if|else|for|while|do|break|continue|switch|case|default|function|return|try|catch|finally|throw|class|extends|new|this|super|import|export|default|null|undefined|true|false|in|of|instanceof|typeof|void|delete|async|await|from|as|yield|static|get|set|constructor)\b/g, builtin: /\b(?:Array|Object|String|Number|Boolean|RegExp|Math|Date|JSON|Promise|Map|Set|WeakMap|WeakSet|Symbol|BigInt|Infinity|NaN|undefined|null|console|window|document|global|process)\b/g, string: /(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, number: /-?\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\d*\.?\d+(?:[Ee][+-]?\d+)?)\b/g, comment: /\/\/.*$|\/\*[\s\S]*?\*\//gm, operator: /=>|[+\-*/%=<>!&|^~?:]+/g, punctuation: /[{}[\]();,.]/g, function: /\b[a-zA-Z_$][\w$]*(?=\s*\()/g, variable: /\b[a-zA-Z_$][\w$]*\b/g }, php: { keyword: /\b(?:namespace|use|class|extends|implements|public|private|protected|static|function|return|if|else|elseif|foreach|for|while|do|switch|case|break|default|continue|try|catch|throw|finally|as|array|new|echo|print|require|include|require_once|include_once)\b/g, variable: /\$[a-z_]\w*/gi, string: /(['"])(?:\\[\s\S]|(?!\1)[^\\])*\1|<<<['"]?\w+['"]?[\s\S]+?\w+[;"']?/g, number: /-?\b\d+(?:\.\d+)?\b/g, operator: /[+\-*\/%=<>!&|^~?:]+|->|::/g, punctuation: /[{}[\]();,.]/g, comment: /\/\/.*$|#.*$|\/\*[\s\S]*?\*\//gm, phpTag: /<\?(?:php)?|\?>/g }, bash: { command: /\b(?:apt|yum|npm|git|docker|systemctl|service|cd|ls|cp|mv|rm|mkdir|chmod|chown|ssh|curl|wget)\b/g, parameter: /--?[a-z-]+/g, string: /(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g, variable: /\$[a-zA-Z0-9_]+|\${[^}]+}/g, comment: /#.*/g, path: /(?:\/[a-zA-Z0-9_.-]+)+/g, operator: /[|>&;]/g }, json: { string: /"(?:\\.|[^"\\])*"(?=\s*:)/g, value: /"(?:\\.|[^"\\])*"/g, number: /-?\b\d+(?:\.\d+)?\b/g, punctuation: /[{}\[\]:,]/g, boolean: /\b(?:true|false|null)\b/g } }, // Event handlers onHighlighted: null, // When Highlight is successful onCopied: null, // When copying the code onError: null, // When an error occurs onLineClick: null, // When clicking on the line onFoldToggle: null, // When opening/off the code folding debug: false }, state: { instances: new Map(), initialized: false, observer: null, currentLocale: 'th', i18n: { 'th': { copyText: 'คัดลอกโค้ด', copiedText: 'คัดลอกแล้ว', errorText: 'เกิดข้อผิดพลาด', loadingText: 'กำลังโหลด...' }, 'en': { copyText: 'Copy code', copiedText: 'Copied!', errorText: 'Error', loadingText: 'Loading' } } }, /** * Translate the current language */ translate(key) { const locale = this.state.currentLocale; const translations = this.state.i18n[locale] || this.state.i18n['en']; return translations[key] || key; }, /** * Create a new Instance of SyntaxHighlightercomponent. */ create(element, options = {}) { if (typeof element === 'string') { element = document.querySelector(element); } if (!element) { console.error('Element not found'); return null; } const existingInstance = this.getInstance(element); if (existingInstance) { return existingInstance; } const instance = { id: 'syntax_' + Math.random().toString(36).substring(2, 11), element, options: {...this.config, ...this.extractOptionsFromElement(element), ...options}, originalContent: null, language: null, highlighted: false, wrapper: null, tokens: [], lineCount: 0, foldedLines: new Set(), markedLines: new Set() }; this.setup(instance); this.state.instances.set(instance.id, instance); element.dataset.syntaxComponentId = instance.id; element.syntaxInstance = instance; return instance; }, /** * Instance setting */ setup(instance) { try { if (instance.element.tagName === 'CODE') { instance.codeElement = instance.element; instance.preElement = instance.element.parentNode.tagName === 'PRE' ? instance.element.parentNode : null; if (!instance.preElement) { instance.preElement = document.createElement('pre'); instance.element.parentNode.insertBefore(instance.preElement, instance.element); instance.preElement.appendChild(instance.element); } } else if (instance.element.tagName === 'PRE') { instance.preElement = instance.element; instance.codeElement = instance.element.querySelector('code'); if (!instance.codeElement) { instance.codeElement = document.createElement('code'); instance.codeElement.textContent = instance.element.textContent; instance.element.textContent = ''; instance.element.appendChild(instance.codeElement); } } else { instance.preElement = document.createElement('pre'); instance.codeElement = document.createElement('code'); instance.codeElement.textContent = instance.element.textContent; instance.preElement.appendChild(instance.codeElement); instance.element.textContent = ''; instance.element.appendChild(instance.preElement); } instance.originalContent = instance.codeElement.textContent; instance.element.classList.add('syntax-highlighter-component'); instance.language = this.detectLanguage(instance); this.highlight(instance); instance.refresh = () => { this.refresh(instance); }; instance.setCode = (code) => { this.setCode(instance, code); }; instance.setLanguage = (language) => { this.setLanguage(instance, language); }; instance.copyCode = () => { this.copyCode(instance); }; instance.highlightLine = (lineNumber) => { this.highlightLine(instance, lineNumber); }; instance.foldCode = (startLine, endLine) => { this.foldCode(instance, startLine, endLine); }; instance.unfoldCode = (lineNumber) => { this.unfoldCode(instance, lineNumber); }; this.dispatchEvent(instance, 'init', { instance }); } catch (error) { console.error('SyntaxHighlighterComponent setup error:', error); instance.error = error.message; this.renderError(instance); } }, /** * Check the language */ detectLanguage(instance) { if (instance.options.language) { return instance.options.language; } const langClass = Array.from(instance.codeElement.classList) .find(cls => cls.startsWith('language-')); if (langClass) { const lang = langClass.replace('language-', ''); if (instance.options.languages.includes(lang)) { return lang; } } const dataLang = instance.codeElement.dataset.language; if (dataLang && instance.options.languages.includes(dataLang)) { return dataLang; } if (instance.options.autoDetect) { const code = instance.originalContent.trim(); for (const [lang, pattern] of Object.entries(instance.options.languagePatterns)) { if (pattern.test(code)) { return lang; } } } return 'plain'; }, /** * Do syntax highlighting */ highlight(instance) { try { if (!instance.language) { throw new Error('Language not detected'); } const processedCode = this.preprocessCode(instance.originalContent, instance.language, instance.options); instance.tokens = this.tokenize(processedCode, instance.language); instance.lineCount = processedCode.split('\n').length; const wrapper = this.createHighlightedWrapper(instance); instance.wrapper = wrapper; instance.preElement.style.display = 'none'; instance.preElement.parentNode.insertBefore(wrapper, instance.preElement.nextSibling); instance.highlighted = true; this.dispatchEvent(instance, 'highlighted', { language: instance.language, code: processedCode }); if (typeof instance.options.onHighlighted === 'function') { instance.options.onHighlighted.call(instance, { language: instance.language, code: processedCode }); } this.applyTheme(instance); } catch (error) { console.error('SyntaxHighlighterComponent highlight error:', error); instance.error = error.message; this.renderError(instance); this.dispatchEvent(instance, 'error', { error: error.message }); if (typeof instance.options.onError === 'function') { instance.options.onError.call(instance, error); } } }, /** * Format the code before highlighting. */ preprocessCode(code, language, options) { code = code.replace(/^\uFEFF/, '').trim(); code = code.replace(/^[\r\n]+|[\r\n]+$/g, ''); if (options.autoIndent) { code = this.autoIndentCode(code, language, options.indentation); } return code; }, /** * Automatically format code */ autoIndentCode(code, language, indentOptions) { const lines = code.split('\n'); const indentSize = indentOptions.size || 2; const indentChar = indentOptions.useTabs ? '\t' : ' '.repeat(indentSize); let indentLevel = 0; let inComment = false; const patterns = { html: { indent: /<[^/!][^>]*>$/, outdent: /^<\//, ignore: /^()/ }, css: { indent: /{$/, outdent: /^}/ }, javascript: { indent: /{$|\($|\[$|=>$/, outdent: /^}|^\)|^\]/, ignore: /^(\/\/|\/\*|\*\/)/ }, php: { indent: /{$/, outdent: /^}/, ignore: /^(\/\/|#|\*)/ }, bash: { indent: /\\$/, ignore: /^#/ }, json: { indent: /[\{\[]$/, outdent: /^[\}\]]/ } }; const pattern = patterns[language] || { indent: /{$|\($|\[$|=>$/, outdent: /^}|^\)|^\]/ }; return lines.map((line /* , _index */) => { const trimmedLine = line.trim(); if (!trimmedLine) return ''; if (pattern.ignore && pattern.ignore.test(trimmedLine)) { return indentChar.repeat(indentLevel) + trimmedLine; } if (trimmedLine.includes('/*')) inComment = true; if (trimmedLine.includes('*/')) { inComment = false; return indentChar.repeat(indentLevel) + trimmedLine; } if (inComment) { return indentChar.repeat(indentLevel) + trimmedLine; } if (pattern.outdent && pattern.outdent.test(trimmedLine)) { indentLevel = Math.max(0, indentLevel - 1); } const indentedLine = indentChar.repeat(indentLevel) + trimmedLine; if (pattern.indent && pattern.indent.test(trimmedLine)) { indentLevel++; } return indentedLine; }).join('\n'); }, /** * Split the code into tokens */ tokenize(code, language) { const patterns = this.config.tokenPatterns[language]; // If no token patterns for this language, return each line as a single text token if (!patterns) { const lines = code.split('\n'); return lines.map(line => [{ type: 'text', content: this.escapeHTML(line) }]); } const lines = code.split('\n'); const result = []; lines.forEach(line => { const lineTokens = this.tokenizeLine(line, patterns); result.push(lineTokens); }); return result; }, /** * Split lines into tokens */ tokenizeLine(line, patterns) { const tokens = []; let remaining = line; while (remaining.length > 0) { let found = false; for (const [type, pattern] of Object.entries(patterns)) { if (pattern instanceof RegExp) { pattern.lastIndex = 0; const match = pattern.exec(remaining); if (match && match.index === 0) { tokens.push({ type, content: this.escapeHTML(match[0]) }); remaining = remaining.substring(match[0].length); found = true; break; } } } if (!found) { tokens.push({ type: 'text', content: this.escapeHTML(remaining.charAt(0)) }); remaining = remaining.substring(1); } } return tokens; }, /** * สร้าง wrapper สำหรับแสดงโค้ด */ createHighlightedWrapper(instance) { const wrapper = document.createElement('div'); wrapper.className = `highlighted-code ${instance.options.themeName}`; wrapper.setAttribute('data-language', instance.language); const header = document.createElement('div'); header.className = 'code-header'; const langLabel = document.createElement('div'); langLabel.className = 'language-label'; langLabel.textContent = instance.language; header.appendChild(langLabel); // Add Run button for JavaScript when EditorSkeleton is available if ((instance.language === 'javascript' || instance.language === 'js' || instance.language === 'html') && window.EditorSkeleton && typeof window.EditorSkeleton.run === 'function') { const runBtn = document.createElement('button'); runBtn.className = 'run-button'; runBtn.title = 'Run code'; runBtn.textContent = 'Run'; runBtn.addEventListener('click', () => { // Open editor with code and immediately run window.EditorSkeleton.init(instance.originalContent, 'javascript'); window.EditorSkeleton.show(); // small delay to ensure iframe and editor initialized setTimeout(() => { window.EditorSkeleton.run(instance.originalContent); }, 60); }); header.appendChild(runBtn); } if (instance.options.copyButton) { const copyBtn = document.createElement('button'); copyBtn.className = 'copy-button'; copyBtn.title = this.translate('copyText'); copyBtn.innerHTML = ` ${this.translate('copyText')}`; copyBtn.addEventListener('click', () => this.copyCode(instance)); header.appendChild(copyBtn); } wrapper.appendChild(header); const content = document.createElement('div'); content.className = 'code-content'; if (instance.options.lineNumbers) { const lineNumbers = document.createElement('div'); lineNumbers.className = 'line-numbers'; for (let i = 0; i < instance.lineCount; i++) { const lineNumber = document.createElement('a'); lineNumber.className = 'line-number'; lineNumber.textContent = i + 1; lineNumber.setAttribute('data-line', i + 1); lineNumber.addEventListener('click', (e) => { this.handleLineClick(instance, i + 1, e); }); lineNumbers.appendChild(lineNumber); } content.appendChild(lineNumbers); } const codeBody = document.createElement('div'); codeBody.className = 'code-body'; if (instance.tokens && instance.tokens.length > 0) { instance.tokens.forEach((lineTokens, lineIndex) => { // Defensive: ensure lineTokens is an array of token objects if (!Array.isArray(lineTokens)) { lineTokens = [{type: 'text', content: this.escapeHTML(String(lineTokens))}]; } const lineElement = document.createElement('div'); lineElement.className = 'line'; lineElement.setAttribute('data-line', lineIndex + 1); lineElement.addEventListener('click', (e) => { if (e.target === lineElement) { this.handleLineClick(instance, lineIndex + 1, e); } }); lineTokens.forEach(token => { const span = document.createElement('span'); span.className = `token ${token.type}`; span.innerHTML = token.content; lineElement.appendChild(span); }); codeBody.appendChild(lineElement); }); } else { const lines = instance.originalContent.split('\n'); lines.forEach((line, lineIndex) => { const lineElement = document.createElement('div'); lineElement.className = 'line'; lineElement.setAttribute('data-line', lineIndex + 1); lineElement.textContent = line; codeBody.appendChild(lineElement); }); } content.appendChild(codeBody); wrapper.appendChild(content); if (instance.options.codeFolding) { this.setupCodeFolding(instance, codeBody); } return wrapper; }, /** * จัดการกับการคลิกที่บรรทัด */ handleLineClick(instance, lineNumber, event) { if (instance.options.highlightLines) { this.toggleLineHighlight(instance, lineNumber); } this.dispatchEvent(instance, 'lineClick', { lineNumber, event }); if (typeof instance.options.onLineClick === 'function') { instance.options.onLineClick.call(instance, lineNumber, event); } }, /** * สลับการเน้นบรรทัด */ toggleLineHighlight(instance, lineNumber) { const lineElements = instance.wrapper.querySelectorAll(`.line[data-line="${lineNumber}"], .line-number[data-line="${lineNumber}"]`); if (instance.markedLines.has(lineNumber)) { lineElements.forEach(el => el.classList.remove('highlighted')); instance.markedLines.delete(lineNumber); } else { lineElements.forEach(el => el.classList.add('highlighted')); instance.markedLines.add(lineNumber); } }, /** * เน้นบรรทัดที่ระบุ */ highlightLine(instance, lineNumber) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; const lineElements = instance.wrapper.querySelectorAll(`.line[data-line="${lineNumber}"], .line-number[data-line="${lineNumber}"]`); lineElements.forEach(el => el.classList.add('highlighted')); instance.markedLines.add(lineNumber); }, /** * ตั้งค่าการพับโค้ด */ setupCodeFolding(instance, codeBody) { const lines = codeBody.querySelectorAll('.line'); lines.forEach((line, index) => { const content = line.textContent; if (content.includes('{') || content.includes('}') || content.includes('function') || content.includes('class')) { const foldMarker = document.createElement('span'); foldMarker.className = 'fold-marker'; foldMarker.textContent = '+'; foldMarker.setAttribute('title', 'Fold code block'); foldMarker.addEventListener('click', (e) => { e.stopPropagation(); const startLine = index + 1; let endLine = this.findMatchingBracket(instance, content, startLine); if (endLine > startLine) { this.foldCode(instance, startLine, endLine); foldMarker.textContent = '-'; foldMarker.setAttribute('title', 'Unfold code block'); } }); line.insertBefore(foldMarker, line.firstChild); } }); }, /** * ค้นหาวงเล็บปิดที่ตรงกัน */ findMatchingBracket(instance, content, startLine) { const lines = instance.wrapper.querySelectorAll('.line'); let bracketCount = 0; for (let i = 0; i < content.length; i++) { if (content[i] === '{') bracketCount++; } for (let i = startLine; i < lines.length; i++) { const lineContent = lines[i].textContent; for (let j = 0; j < lineContent.length; j++) { if (lineContent[j] === '{') bracketCount++; if (lineContent[j] === '}') { bracketCount--; if (bracketCount === 0) { return i + 1; } } } } return startLine; }, /** * พับโค้ด */ foldCode(instance, startLine, endLine) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; for (let i = startLine; i < endLine; i++) { const lineElements = instance.wrapper.querySelectorAll(`.line[data-line="${i}"], .line-number[data-line="${i}"]`); lineElements.forEach(el => el.classList.add('folded')); instance.foldedLines.add(i); } const lineBeforeFold = instance.wrapper.querySelector(`.line[data-line="${startLine - 1}"]`); if (lineBeforeFold) { const foldIndicator = document.createElement('div'); foldIndicator.className = 'fold-indicator'; foldIndicator.textContent = `... ${endLine - startLine} lines folded ...`; foldIndicator.setAttribute('data-fold-start', startLine); foldIndicator.setAttribute('data-fold-end', endLine); foldIndicator.addEventListener('click', () => { this.unfoldCode(instance, startLine); }); lineBeforeFold.after(foldIndicator); } this.dispatchEvent(instance, 'foldToggle', { startLine, endLine, folded: true }); if (typeof instance.options.onFoldToggle === 'function') { instance.options.onFoldToggle.call(instance, startLine, endLine, true); } }, /** * ยกเลิกการพับโค้ด */ unfoldCode(instance, startLine) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; const foldIndicator = instance.wrapper.querySelector(`.fold-indicator[data-fold-start="${startLine}"]`); if (!foldIndicator) return; const endLine = parseInt(foldIndicator.getAttribute('data-fold-end')); for (let i = startLine; i < endLine; i++) { const lineElements = instance.wrapper.querySelectorAll(`.line[data-line="${i}"], .line-number[data-line="${i}"]`); lineElements.forEach(el => el.classList.remove('folded')); instance.foldedLines.delete(i); } foldIndicator.remove(); const foldMarker = instance.wrapper.querySelector(`.line[data-line="${startLine - 1}"] .fold-marker`); if (foldMarker) { foldMarker.textContent = '+'; foldMarker.setAttribute('title', 'Fold code block'); } this.dispatchEvent(instance, 'foldToggle', { startLine, endLine, folded: false }); if (typeof instance.options.onFoldToggle === 'function') { instance.options.onFoldToggle.call(instance, startLine, endLine, false); } }, /** * ปรับใช้ธีม */ applyTheme(instance) { if (!instance.options.themeName) return; if (instance.wrapper) { instance.wrapper.className = `highlighted-code ${instance.options.themeName}`; } }, /** * Escape HTML */ escapeHTML(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, /** * แสดงข้อผิดพลาด */ renderError(instance) { const errorDiv = document.createElement('div'); errorDiv.className = 'syntax-error'; errorDiv.textContent = this.translate('errorText') + ': ' + instance.error; errorDiv.style.cssText = 'color: #e74c3c; background-color: #fceae9; padding: 10px; border: 1px solid #e74c3c; border-radius: 4px; margin: 10px 0;'; if (instance.wrapper) { instance.wrapper.parentNode.replaceChild(errorDiv, instance.wrapper); instance.wrapper = errorDiv; } else { instance.preElement.style.display = 'none'; instance.preElement.parentNode.insertBefore(errorDiv, instance.preElement.nextSibling); instance.wrapper = errorDiv; } }, /** * คัดลอกโค้ดไปยัง clipboard */ copyCode(instance) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; try { const text = instance.originalContent; navigator.clipboard.writeText(text).then(() => { const copyBtn = instance.wrapper.querySelector('.copy-button'); if (copyBtn) { copyBtn.textContent = this.translate('copiedText'); setTimeout(() => { copyBtn.innerHTML = ` ${this.translate('copyText')}`; }, 2000); } this.dispatchEvent(instance, 'copied', {code: text}); if (typeof instance.options.onCopied === 'function') { instance.options.onCopied.call(instance, text); } }).catch(err => { console.error('Copy failed:', err); }); } catch (error) { console.error('Copy error:', error); } }, /** * รีเฟรชการแสดงผล */ refresh(instance) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; if (instance.wrapper) { instance.wrapper.remove(); instance.wrapper = null; } instance.preElement.style.display = ''; this.highlight(instance); }, /** * เปลี่ยนโค้ด */ setCode(instance, code) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; instance.originalContent = code; instance.codeElement.textContent = code; this.refresh(instance); return instance; }, /** * เปลี่ยนภาษา */ setLanguage(instance, language) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; instance.language = language; instance.options.language = language; instance.codeElement.className = ''; instance.codeElement.classList.add(`language-${language}`); this.refresh(instance); return instance; }, /** * เปลี่ยนธีม */ setTheme(instance, themeName) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return; instance.options.themeName = themeName; if (instance.wrapper) { instance.wrapper.className = `highlighted-code ${themeName}`; } return instance; }, /** * ดึงตัวเลือกจาก data attributes */ extractOptionsFromElement(element) { const options = {}; const dataset = element.dataset; if (dataset.props) { try { const props = JSON.parse(dataset.props); Object.assign(options, props); } catch (e) { console.warn('Invalid JSON in data-props:', e); } } if (dataset.language) options.language = dataset.language; if (dataset.autoDetect !== undefined) options.autoDetect = dataset.autoDetect === 'true'; if (dataset.lineNumbers !== undefined) options.lineNumbers = dataset.lineNumbers === 'true'; if (dataset.copyButton !== undefined) options.copyButton = dataset.copyButton === 'true'; if (dataset.highlightLines !== undefined) options.highlightLines = dataset.highlightLines === 'true'; if (dataset.autoIndent !== undefined) options.autoIndent = dataset.autoIndent === 'true'; if (dataset.themeName) options.themeName = dataset.themeName; if (dataset.codeFolding !== undefined) options.codeFolding = dataset.codeFolding === 'true'; if (dataset.indentSize) options.indentation = {...(options.indentation || {}), size: parseInt(dataset.indentSize)}; if (dataset.indentTabs !== undefined) options.indentation = {...(options.indentation || {}), useTabs: dataset.indentTabs === 'true'}; return options; }, /** * ค้นหา instance จาก element */ getInstance(element) { if (typeof element === 'string') { element = document.querySelector(element); } if (!element) return null; if (element.syntaxInstance) { return element.syntaxInstance; } const id = element.dataset.syntaxComponentId; if (id && this.state.instances.has(id)) { return this.state.instances.get(id); } for (const instance of this.state.instances.values()) { if (instance.element === element) { return instance; } } return null; }, /** * ส่งเหตุการณ์ */ dispatchEvent(instance, eventName, detail = {}) { if (!instance.element) return; const event = new CustomEvent(`syntax:${eventName}`, { bubbles: true, cancelable: true, detail: { instance, ...detail } }); instance.element.dispatchEvent(event); }, /** * ลบ instance */ destroy(instance) { if (typeof instance === 'string') { instance = this.state.instances.get(instance); } else if (instance instanceof HTMLElement) { instance = this.getInstance(instance); } if (!instance) return false; if (instance.wrapper) { instance.wrapper.remove(); } instance.preElement.style.display = ''; instance.highlighted = false; instance.wrapper = null; instance.tokens = []; instance.markedLines.clear(); instance.foldedLines.clear(); if (instance.element) { delete instance.element.syntaxInstance; delete instance.element.dataset.syntaxComponentId; instance.element.classList.remove('syntax-highlighter-component'); } this.dispatchEvent(instance, 'destroy'); if (instance.id) { this.state.instances.delete(instance.id); } return true; }, /** * ตั้งค่า observer */ setupObserver() { if (this.state.observer) { this.state.observer.disconnect(); } this.state.observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.tagName === 'CODE' && node.parentNode.tagName === 'PRE') { this.create(node); } else if (node.tagName === 'PRE') { const codeElement = node.querySelector('code'); if (codeElement) { this.create(codeElement); } } else { const codeElements = node.querySelectorAll('pre > code'); codeElements.forEach(code => this.create(code)); const syntaxElements = node.querySelectorAll('[data-component="syntaxhighlighter"]'); syntaxElements.forEach(el => this.create(el)); } } }); }); }); this.state.observer.observe(document.body, { childList: true, subtree: true }); }, /** * เริ่มต้น elements ที่มี data-component="syntaxhighlighter" */ initElements() { document.querySelectorAll('[data-component="syntaxhighlighter"]').forEach(element => { this.create(element); }); document.querySelectorAll('pre > code:not(.highlighted)').forEach(element => { const hasLanguageClass = Array.from(element.classList).some(cls => cls.startsWith('language-')); if (hasLanguageClass || this.config.autoDetect) { this.create(element); } }); }, /** * ตั้งค่าเริ่มต้น SyntaxHighlighterComponent */ async init(options = {}) { this.config = {...this.config, ...options}; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.initElements()); } else { this.initElements(); } this.setupObserver(); this.state.initialized = true; return this; }, /** * อัพเดต UI ตามภาษาปัจจุบัน */ updateUIText() { document.querySelectorAll('.highlighted-code .copy-button').forEach(button => { button.title = this.translate('copyText'); button.innerHTML = ` ${this.translate('copyText')}`; }); }, /** * Clean when not in use. */ cleanup() { if (this.state.observer) { this.state.observer.disconnect(); this.state.observer = null; } this.state.instances.forEach(instance => { this.destroy(instance); }); this.state.instances.clear(); this.state.initialized = false; } }; /** * Register Component with ComponentManager */ if (window.ComponentManager) { const syntaxHighlighterComponentDefinition = { template: null, validElement(element) { return element.classList.contains('syntax-highlighter-component') || element.dataset.component === 'syntaxhighlighter' || (element.tagName === 'CODE' && element.parentNode.tagName === 'PRE'); }, setupElement(element /* , _state */) { const options = SyntaxHighlighterComponent.extractOptionsFromElement(element); const syntaxComponent = SyntaxHighlighterComponent.create(element, options); element._syntaxComponent = syntaxComponent; return element; }, beforeDestroy() { if (this.element && this.element._syntaxComponent) { SyntaxHighlighterComponent.destroy(this.element._syntaxComponent); delete this.element._syntaxComponent; } } }; ComponentManager.define('syntaxhighlighter', syntaxHighlighterComponentDefinition); } /** * Make it usable directly */ window.SyntaxHighlighterComponent = SyntaxHighlighterComponent;