/* 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: /^</,
css: /^(\.|#|\*|@media|@keyframes|body|html)/,
javascript: /^(?:import|export|class|function|const|let|var|if|for|while)/,
php: /^(?:<\?|namespace|use)/,
bash: /^(?:#!\s*\/bin\/|\$|sudo|apt|yum|npm|git)/,
json: /^[\{\[]/,
xml: /^<\?xml/,
sql: /^(?:SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i,
python: /^(?:import|from|def|class|if|for|while|print)/,
ruby: /^(?:require|class|module|def|if|unless|puts)/
},
indentation: {
size: 2,
useTabs: false,
automaticIndent: true
},
tokenPatterns: {
html: {
tag: /<\/?[a-z0-9-]+|>/gi,
attribute: /\s+([a-z0-9-]+)(?:=(?:["'](?:\\.|[^\\"])*["']|[^\s"'>]+))?/gi,
string: /"[^"]*"|'[^']*'/g,
comment: /<!--[\s\S]*?-->/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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> ${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, '"')
.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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> ${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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg> ${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;