| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
transform:'text-transform', decoration:'text-decoration', |
| 7 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 8 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 9 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 10 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 11 |
}; |
| 12 |
var _typoUnits = { size:'sizeUnit', lineHeight:'lineHeightUnit', letterSpacing:'letterSpacingUnit', wordSpacing:'wordSpacingUnit' }; |
| 13 |
var _typoUnitDefaults = { size:'px', lineHeight:'', letterSpacing:'px', wordSpacing:'px' }; |
| 14 |
function typoCssVarsForEl(el, obj, prefix) { |
| 15 |
if (!obj || typeof obj !== 'object') return; |
| 16 |
Object.keys(_typoKeys).forEach(function (k) { |
| 17 |
var v = obj[k]; if (v === undefined || v === '') return; |
| 18 |
var prop = _typoKeys[k]; |
| 19 |
var base = k.replace(/Desktop|Tablet|Mobile/, ''); |
| 20 |
var uKey = _typoUnits[base]; |
| 21 |
if (uKey && typeof v === 'number') v = v + (obj[uKey] || _typoUnitDefaults[base] || ''); |
| 22 |
el.style.setProperty(prefix + prop, v); |
| 23 |
}); |
| 24 |
} |
| 25 |
|
| 26 |
// ── Token colours ────────────────────────────────────────────────────────── |
| 27 |
var TOKEN_COLOURS = { |
| 28 |
plain: null, |
| 29 |
keyword: '#c792ea', |
| 30 |
string: '#c3e88d', |
| 31 |
comment: '#546e7a', |
| 32 |
number: '#f78c6c', |
| 33 |
operator: '#89ddff', |
| 34 |
function: '#82aaff', |
| 35 |
tag: '#f07178', |
| 36 |
variable: '#ffcb6b' |
| 37 |
}; |
| 38 |
|
| 39 |
var LANG_KW = { |
| 40 |
javascript: 'break case catch class const continue debugger default delete do else enum export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield async await', |
| 41 |
typescript: 'break case catch class const continue debugger default delete do else enum export extends false finally for from function if import in instanceof interface let namespace new null of return static super switch this throw true try type typeof undefined var void while with yield async await', |
| 42 |
php: 'echo print class function return if else elseif for foreach while do switch case break continue true false null new public private protected static abstract final try catch finally throw namespace use extends implements interface trait', |
| 43 |
python: 'False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield', |
| 44 |
css: 'important', |
| 45 |
bash: 'if then else elif fi for while do done case esac in function return export readonly local true false', |
| 46 |
sql: 'SELECT FROM WHERE JOIN LEFT RIGHT INNER OUTER ON UPDATE INSERT INTO VALUES DELETE CREATE DROP TABLE ALTER ADD COLUMN AS GROUP BY ORDER HAVING LIMIT DISTINCT WITH UNION ALL SET AND OR NOT NULL IS IN EXISTS', |
| 47 |
ruby: 'BEGIN END __ENCODING__ __END__ __FILE__ __LINE__ alias and begin break case class def defined do else elsif end ensure false for if in module next nil not or raise redo rescue retry return self super then true undef unless until when while yield', |
| 48 |
go: 'break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false', |
| 49 |
rust: 'as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while' |
| 50 |
}; |
| 51 |
|
| 52 |
function tokenize(code, lang) { |
| 53 |
if (!lang || lang === 'text') return [{ type: 'plain', text: code }]; |
| 54 |
var kw = (LANG_KW[lang] || '').split(' ').filter(Boolean); |
| 55 |
var rules = [ |
| 56 |
{ type: 'string', re: /("""[\s\S]*?"""|'''[\s\S]*?'''|`[\s\S]*?`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/ }, |
| 57 |
{ type: 'comment', re: /(\/\/[^\n]*|\/\*[\s\S]*?\*\/|#[^\n]*|--[^\n]*)/ }, |
| 58 |
{ type: 'number', re: /(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/ }, |
| 59 |
{ type: 'operator', re: /([+\-*/%=<>!&|^~?:;,.()\[\]{}])/ }, |
| 60 |
{ type: 'keyword', re: new RegExp('\\b(' + (kw.length ? kw.join('|') : '__NONE__') + ')\\b') }, |
| 61 |
{ type: 'function', re: /\b([a-zA-Z_$][a-zA-Z0-9_$]*)(?=\s*\()/ }, |
| 62 |
lang === 'html' |
| 63 |
? { type: 'tag', re: /(<\/?\w[^>]*>)/ } |
| 64 |
: null, |
| 65 |
lang === 'php' |
| 66 |
? { type: 'variable', re: /(\$[a-zA-Z_]\w*)/ } |
| 67 |
: null |
| 68 |
].filter(Boolean); |
| 69 |
|
| 70 |
var tokens = []; |
| 71 |
var rest = code; |
| 72 |
while (rest.length) { |
| 73 |
var bestIdx = Infinity, bestLen = 0, bestType = 'plain', bestM = null; |
| 74 |
for (var i = 0; i < rules.length; i++) { |
| 75 |
var m = rules[i].re.exec(rest); |
| 76 |
if (m && m.index < bestIdx) { |
| 77 |
bestIdx = m.index; bestLen = m[0].length; bestType = rules[i].type; bestM = m; |
| 78 |
} |
| 79 |
} |
| 80 |
if (!bestM) { tokens.push({ type: 'plain', text: rest }); break; } |
| 81 |
if (bestIdx > 0) tokens.push({ type: 'plain', text: rest.slice(0, bestIdx) }); |
| 82 |
tokens.push({ type: bestType, text: rest.slice(bestIdx, bestIdx + bestLen) }); |
| 83 |
rest = rest.slice(bestIdx + bestLen); |
| 84 |
} |
| 85 |
return tokens; |
| 86 |
} |
| 87 |
|
| 88 |
function buildPaneDOM(code, lang, opts, side, diffLinesArr) { |
| 89 |
var lines = code.split('\n'); |
| 90 |
var fragment = document.createDocumentFragment(); |
| 91 |
|
| 92 |
lines.forEach(function (line, i) { |
| 93 |
var lineEl = document.createElement('div'); |
| 94 |
lineEl.className = 'bkbg-cc-line' + (opts.wrapLines ? ' bkbg-cc-wrap-lines' : ''); |
| 95 |
|
| 96 |
var isDiff = diffLinesArr.indexOf(i) !== -1; |
| 97 |
if (isDiff && side === 'left') lineEl.style.backgroundColor = opts.diffRemBg; |
| 98 |
if (isDiff && side === 'right') lineEl.style.backgroundColor = opts.diffAddBg; |
| 99 |
|
| 100 |
if (opts.showLineNumbers) { |
| 101 |
var numEl = document.createElement('span'); |
| 102 |
numEl.className = 'bkbg-cc-line-num'; |
| 103 |
numEl.style.color = opts.lineNumColor; |
| 104 |
numEl.textContent = i + 1; |
| 105 |
lineEl.appendChild(numEl); |
| 106 |
} |
| 107 |
|
| 108 |
if (opts.showDiffMarkers && isDiff) { |
| 109 |
var marker = document.createElement('span'); |
| 110 |
marker.className = 'bkbg-cc-line-marker ' + (side === 'left' ? 'bkbg-cc-removed' : 'bkbg-cc-added'); |
| 111 |
marker.textContent = side === 'left' ? '−' : '+'; |
| 112 |
lineEl.appendChild(marker); |
| 113 |
} |
| 114 |
|
| 115 |
var codeSpan = document.createElement('span'); |
| 116 |
codeSpan.className = 'bkbg-cc-line-code'; |
| 117 |
codeSpan.style.color = opts.codeColor; |
| 118 |
|
| 119 |
var tokens = tokenize(line, lang); |
| 120 |
tokens.forEach(function (tok) { |
| 121 |
var span = document.createElement('span'); |
| 122 |
span.className = 'bkbg-cc-tok-' + tok.type; |
| 123 |
var col = TOKEN_COLOURS[tok.type]; |
| 124 |
if (col) span.style.color = col; |
| 125 |
span.textContent = tok.text; |
| 126 |
codeSpan.appendChild(span); |
| 127 |
}); |
| 128 |
|
| 129 |
lineEl.appendChild(codeSpan); |
| 130 |
fragment.appendChild(lineEl); |
| 131 |
}); |
| 132 |
|
| 133 |
return fragment; |
| 134 |
} |
| 135 |
|
| 136 |
function buildPane(code, lang, label, opts, side, diffLinesArr) { |
| 137 |
var pane = document.createElement('div'); |
| 138 |
pane.className = 'bkbg-cc-pane'; |
| 139 |
pane.style.backgroundColor = side === 'left' ? opts.bgLeft : opts.bgRight; |
| 140 |
pane.dataset.side = side; |
| 141 |
pane.dataset.code = code; |
| 142 |
|
| 143 |
// Header |
| 144 |
var header = document.createElement('div'); |
| 145 |
header.className = 'bkbg-cc-header'; |
| 146 |
header.style.backgroundColor = side === 'left' ? opts.headerBgLeft : opts.headerBgRight; |
| 147 |
header.style.color = opts.headerColor; |
| 148 |
|
| 149 |
var titleEl = document.createElement('span'); |
| 150 |
titleEl.textContent = label; |
| 151 |
header.appendChild(titleEl); |
| 152 |
|
| 153 |
var meta = document.createElement('div'); |
| 154 |
meta.className = 'bkbg-cc-header-meta'; |
| 155 |
|
| 156 |
if (opts.showLanguageBadge) { |
| 157 |
var badge = document.createElement('span'); |
| 158 |
badge.className = 'bkbg-cc-lang-badge'; |
| 159 |
badge.style.color = opts.headerColor; |
| 160 |
badge.textContent = lang.toUpperCase(); |
| 161 |
meta.appendChild(badge); |
| 162 |
} |
| 163 |
if (opts.showCopyButtons) { |
| 164 |
var copyBtn = document.createElement('button'); |
| 165 |
copyBtn.className = 'bkbg-cc-copy-btn'; |
| 166 |
copyBtn.style.color = opts.headerColor; |
| 167 |
copyBtn.textContent = 'Copy'; |
| 168 |
copyBtn.addEventListener('click', function () { |
| 169 |
navigator.clipboard.writeText(code).then(function () { |
| 170 |
copyBtn.textContent = 'Copied!'; |
| 171 |
copyBtn.classList.add('bkbg-cc-copied'); |
| 172 |
setTimeout(function () { |
| 173 |
copyBtn.textContent = 'Copy'; |
| 174 |
copyBtn.classList.remove('bkbg-cc-copied'); |
| 175 |
}, 1600); |
| 176 |
}).catch(function () { |
| 177 |
var ta = document.createElement('textarea'); |
| 178 |
ta.value = code; |
| 179 |
document.body.appendChild(ta); |
| 180 |
ta.select(); |
| 181 |
document.execCommand('copy'); |
| 182 |
document.body.removeChild(ta); |
| 183 |
copyBtn.textContent = 'Copied!'; |
| 184 |
setTimeout(function () { copyBtn.textContent = 'Copy'; }, 1600); |
| 185 |
}); |
| 186 |
}); |
| 187 |
meta.appendChild(copyBtn); |
| 188 |
} |
| 189 |
|
| 190 |
header.appendChild(meta); |
| 191 |
pane.appendChild(header); |
| 192 |
|
| 193 |
// Code body |
| 194 |
var body = document.createElement('div'); |
| 195 |
body.className = 'bkbg-cc-code-body'; |
| 196 |
if (opts.maxHeight) body.style.maxHeight = opts.maxHeight + 'px'; |
| 197 |
|
| 198 |
body.appendChild(buildPaneDOM(code, lang, opts, side, diffLinesArr)); |
| 199 |
pane.appendChild(body); |
| 200 |
|
| 201 |
return pane; |
| 202 |
} |
| 203 |
|
| 204 |
function initCodeComparison(appEl) { |
| 205 |
var raw = appEl.dataset.opts; |
| 206 |
if (!raw) return; |
| 207 |
var opts; |
| 208 |
try { opts = JSON.parse(raw); } catch (e) { return; } |
| 209 |
|
| 210 |
// Parse diff lines |
| 211 |
var diffLinesArr = []; |
| 212 |
if (opts.diffLines && opts.diffLines.trim()) { |
| 213 |
opts.diffLines.split(',').forEach(function (s) { |
| 214 |
var n = parseInt(s.trim(), 10); |
| 215 |
if (!isNaN(n)) diffLinesArr.push(n - 1); |
| 216 |
}); |
| 217 |
} |
| 218 |
|
| 219 |
var wrap = document.createElement('div'); |
| 220 |
wrap.className = 'bkbg-cc-wrap bkbg-cc-theme-' + (opts.theme || 'dark'); |
| 221 |
wrap.style.borderRadius = (opts.borderRadius || 12) + 'px'; |
| 222 |
wrap.style.overflow = 'hidden'; |
| 223 |
if (opts.typoCode) typoCssVarsForEl(wrap, opts.typoCode, '--bkbg-cc-cd-'); |
| 224 |
|
| 225 |
var inner = document.createElement('div'); |
| 226 |
inner.className = 'bkbg-cc-inner'; |
| 227 |
|
| 228 |
// Panes |
| 229 |
var leftPaneEl = buildPane(opts.leftCode || '', opts.leftLang || 'javascript', opts.leftLabel || 'Before', opts, 'left', diffLinesArr); |
| 230 |
var rightPaneEl = buildPane(opts.rightCode || '', opts.rightLang || 'javascript', opts.rightLabel || 'After', opts, 'right', diffLinesArr); |
| 231 |
|
| 232 |
var splitL = opts.splitRatio || 50; |
| 233 |
leftPaneEl.style.width = splitL + '%'; |
| 234 |
leftPaneEl.style.flexShrink = '0'; |
| 235 |
leftPaneEl.style.flexGrow = '0'; |
| 236 |
rightPaneEl.style.width = (100 - splitL) + '%'; |
| 237 |
rightPaneEl.style.flexShrink = '0'; |
| 238 |
rightPaneEl.style.flexGrow = '0'; |
| 239 |
|
| 240 |
inner.appendChild(leftPaneEl); |
| 241 |
|
| 242 |
// Divider |
| 243 |
var divider = document.createElement('div'); |
| 244 |
divider.className = 'bkbg-cc-divider'; |
| 245 |
|
| 246 |
if (opts.draggableSplit) { |
| 247 |
var dragging = false, startX = 0, startL = splitL; |
| 248 |
|
| 249 |
divider.addEventListener('mousedown', function (e) { |
| 250 |
dragging = true; |
| 251 |
startX = e.clientX; |
| 252 |
startL = parseFloat(leftPaneEl.style.width); |
| 253 |
divider.classList.add('bkbg-cc-dragging'); |
| 254 |
document.body.style.userSelect = 'none'; |
| 255 |
document.body.style.cursor = 'col-resize'; |
| 256 |
e.preventDefault(); |
| 257 |
}); |
| 258 |
document.addEventListener('mousemove', function (e) { |
| 259 |
if (!dragging) return; |
| 260 |
var dx = e.clientX - startX; |
| 261 |
var totalW = inner.getBoundingClientRect().width; |
| 262 |
var newL = Math.min(80, Math.max(20, startL + (dx / totalW * 100))); |
| 263 |
leftPaneEl.style.width = newL + '%'; |
| 264 |
rightPaneEl.style.width = (100 - newL) + '%'; |
| 265 |
}); |
| 266 |
document.addEventListener('mouseup', function () { |
| 267 |
if (!dragging) return; |
| 268 |
dragging = false; |
| 269 |
divider.classList.remove('bkbg-cc-dragging'); |
| 270 |
document.body.style.userSelect = ''; |
| 271 |
document.body.style.cursor = ''; |
| 272 |
}); |
| 273 |
} |
| 274 |
|
| 275 |
inner.appendChild(divider); |
| 276 |
inner.appendChild(rightPaneEl); |
| 277 |
wrap.appendChild(inner); |
| 278 |
|
| 279 |
appEl.parentNode.replaceChild(wrap, appEl); |
| 280 |
} |
| 281 |
|
| 282 |
document.addEventListener('DOMContentLoaded', function () { |
| 283 |
document.querySelectorAll('.bkbg-cc-app').forEach(initCodeComparison); |
| 284 |
}); |
| 285 |
|
| 286 |
if (document.readyState === 'complete' || document.readyState === 'interactive') { |
| 287 |
document.querySelectorAll('.bkbg-cc-app').forEach(initCodeComparison); |
| 288 |
} |
| 289 |
})(); |
| 290 |
|