| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
decoration:'text-decoration', transform:'text-transform', |
| 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 |
function typoCssVarsForEl(el, obj, prefix) { |
| 13 |
if (!obj || typeof obj !== 'object') return; |
| 14 |
Object.keys(_typoKeys).forEach(function (k) { |
| 15 |
var v = obj[k]; |
| 16 |
if (v === undefined || v === '' || v === null) return; |
| 17 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 18 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 19 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 20 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 21 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 22 |
}); |
| 23 |
} |
| 24 |
|
| 25 |
/* ─── Lightweight Markdown Parser ─── */ |
| 26 |
function parseMarkdown(md) { |
| 27 |
var lines = md.split('\n'); |
| 28 |
var html = ''; |
| 29 |
var i = 0; |
| 30 |
|
| 31 |
function escHtml(s) { |
| 32 |
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 33 |
} |
| 34 |
|
| 35 |
function inlineFormat(s) { |
| 36 |
s = escHtml(s); |
| 37 |
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">'); |
| 38 |
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); |
| 39 |
s = s.replace(/`([^`]+)`/g, '<code>$1</code>'); |
| 40 |
s = s.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>'); |
| 41 |
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); |
| 42 |
s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>'); |
| 43 |
s = s.replace(/___([^_]+)___/g, '<strong><em>$1</em></strong>'); |
| 44 |
s = s.replace(/__([^_]+)__/g, '<strong>$1</strong>'); |
| 45 |
s = s.replace(/_([^_]+)_/g, '<em>$1</em>'); |
| 46 |
s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>'); |
| 47 |
s = s.replace(/ $/, '<br>'); |
| 48 |
return s; |
| 49 |
} |
| 50 |
|
| 51 |
while (i < lines.length) { |
| 52 |
var line = lines[i]; |
| 53 |
|
| 54 |
/* Fenced code block */ |
| 55 |
if (/^```/.test(line)) { |
| 56 |
var lang = line.slice(3).trim(); |
| 57 |
var code = ''; |
| 58 |
i++; |
| 59 |
while (i < lines.length && !/^```/.test(lines[i])) { |
| 60 |
code += escHtml(lines[i]) + '\n'; |
| 61 |
i++; |
| 62 |
} |
| 63 |
html += '<pre><code' + (lang ? ' class="language-' + escHtml(lang) + '"' : '') + '>' + code + '</code></pre>'; |
| 64 |
i++; |
| 65 |
continue; |
| 66 |
} |
| 67 |
|
| 68 |
/* ATX Headings */ |
| 69 |
var hm = line.match(/^(#{1,6})\s+(.+)/); |
| 70 |
if (hm) { |
| 71 |
var level = hm[1].length; |
| 72 |
html += '<h' + level + '>' + inlineFormat(hm[2].trim()) + '</h' + level + '>'; |
| 73 |
i++; |
| 74 |
continue; |
| 75 |
} |
| 76 |
|
| 77 |
/* Horizontal rule */ |
| 78 |
if (/^([-*_]){3,}\s*$/.test(line)) { |
| 79 |
html += '<hr>'; |
| 80 |
i++; |
| 81 |
continue; |
| 82 |
} |
| 83 |
|
| 84 |
/* Blockquote */ |
| 85 |
if (/^>\s?/.test(line)) { |
| 86 |
var bq = ''; |
| 87 |
while (i < lines.length && /^>\s?/.test(lines[i])) { |
| 88 |
bq += lines[i].replace(/^>\s?/, '') + '\n'; |
| 89 |
i++; |
| 90 |
} |
| 91 |
html += '<blockquote>' + parseMarkdown(bq.trim()) + '</blockquote>'; |
| 92 |
continue; |
| 93 |
} |
| 94 |
|
| 95 |
/* Unordered list */ |
| 96 |
if (/^[-*+]\s/.test(line)) { |
| 97 |
html += '<ul>'; |
| 98 |
while (i < lines.length && /^[-*+]\s/.test(lines[i])) { |
| 99 |
html += '<li>' + inlineFormat(lines[i].replace(/^[-*+]\s/, '')) + '</li>'; |
| 100 |
i++; |
| 101 |
} |
| 102 |
html += '</ul>'; |
| 103 |
continue; |
| 104 |
} |
| 105 |
|
| 106 |
/* Ordered list */ |
| 107 |
if (/^\d+\.\s/.test(line)) { |
| 108 |
html += '<ol>'; |
| 109 |
while (i < lines.length && /^\d+\.\s/.test(lines[i])) { |
| 110 |
html += '<li>' + inlineFormat(lines[i].replace(/^\d+\.\s/, '')) + '</li>'; |
| 111 |
i++; |
| 112 |
} |
| 113 |
html += '</ol>'; |
| 114 |
continue; |
| 115 |
} |
| 116 |
|
| 117 |
/* Table (GFM) */ |
| 118 |
if (/\|/.test(line) && i + 1 < lines.length && /^\|?[-:| ]+\|/.test(lines[i + 1])) { |
| 119 |
var headers = line.split('|').filter(function (c, idx, arr) { return idx > 0 || c.trim(); }).map(function (c) { return c.trim(); }); |
| 120 |
i += 2; |
| 121 |
html += '<table><thead><tr>'; |
| 122 |
headers.forEach(function (h) { html += '<th>' + inlineFormat(h) + '</th>'; }); |
| 123 |
html += '</tr></thead><tbody>'; |
| 124 |
while (i < lines.length && /\|/.test(lines[i])) { |
| 125 |
var cells = lines[i].split('|').filter(function (c, idx) { return idx > 0 || c.trim(); }).map(function (c) { return c.trim(); }); |
| 126 |
html += '<tr>'; |
| 127 |
cells.forEach(function (c) { html += '<td>' + inlineFormat(c) + '</td>'; }); |
| 128 |
html += '</tr>'; |
| 129 |
i++; |
| 130 |
} |
| 131 |
html += '</tbody></table>'; |
| 132 |
continue; |
| 133 |
} |
| 134 |
|
| 135 |
/* Empty line → paragraph separator */ |
| 136 |
if (/^\s*$/.test(line)) { |
| 137 |
html += ''; |
| 138 |
i++; |
| 139 |
continue; |
| 140 |
} |
| 141 |
|
| 142 |
/* Paragraph */ |
| 143 |
var para = ''; |
| 144 |
while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^#{1,6}\s/.test(lines[i]) && !/^[-*+]\s/.test(lines[i]) && !/^\d+\.\s/.test(lines[i]) && !/^>/.test(lines[i]) && !/^```/.test(lines[i]) && !/^([-*_]){3,}\s*$/.test(lines[i]) && !/\|/.test(lines[i])) { |
| 145 |
if (para) para += ' '; |
| 146 |
para += lines[i]; |
| 147 |
i++; |
| 148 |
} |
| 149 |
if (para) html += '<p>' + inlineFormat(para) + '</p>'; |
| 150 |
} |
| 151 |
|
| 152 |
return html; |
| 153 |
} |
| 154 |
|
| 155 |
|
| 156 |
function initBlock(root) { |
| 157 |
var opts; |
| 158 |
try { opts = JSON.parse(root.getAttribute('data-opts')); } catch (e) { return; } |
| 159 |
var a = opts; |
| 160 |
|
| 161 |
root.innerHTML = ''; |
| 162 |
|
| 163 |
var wrap = document.createElement('div'); |
| 164 |
wrap.className = 'bkbg-mdp-wrap'; |
| 165 |
wrap.style.cssText = 'max-width:' + a.contentMaxWidth + 'px;margin:0 auto;'; |
| 166 |
typoCssVarsForEl(wrap, a.titleTypo, '--bkbg-mdp-tt-'); |
| 167 |
root.appendChild(wrap); |
| 168 |
|
| 169 |
if (a.showTitle) { |
| 170 |
var h = document.createElement('div'); |
| 171 |
h.className = 'bkbg-mdp-title'; |
| 172 |
h.style.color = a.titleColor; |
| 173 |
h.textContent = a.title; |
| 174 |
wrap.appendChild(h); |
| 175 |
} |
| 176 |
|
| 177 |
var container = document.createElement('div'); |
| 178 |
container.className = 'bkbg-mdp-container'; |
| 179 |
container.style.cssText = 'border-color:' + a.borderColor + ';'; |
| 180 |
wrap.appendChild(container); |
| 181 |
|
| 182 |
/* Toolbar */ |
| 183 |
if (a.showToolbar) { |
| 184 |
var toolbar = document.createElement('div'); |
| 185 |
toolbar.className = 'bkbg-mdp-toolbar'; |
| 186 |
toolbar.style.cssText = 'background:' + a.toolbarBg + ';color:' + a.toolbarColor + ';'; |
| 187 |
container.appendChild(toolbar); |
| 188 |
|
| 189 |
var tools = [ |
| 190 |
{ label: 'B', title: 'Bold', prefix: '**', suffix: '**', wrap: true }, |
| 191 |
{ label: 'I', title: 'Italic', prefix: '_', suffix: '_', wrap: true }, |
| 192 |
{ label: 'S', title: 'Strikethrough',prefix: '~~', suffix: '~~', wrap: true }, |
| 193 |
{ label: 'H1', title: 'Heading 1', line: '# ' }, |
| 194 |
{ label: 'H2', title: 'Heading 2', line: '## ' }, |
| 195 |
{ label: 'H3', title: 'Heading 3', line: '### ' }, |
| 196 |
{ label: '{ }', title: 'Inline Code', prefix: '`', suffix: '`', wrap: true }, |
| 197 |
{ label: '🔗', title: 'Link', prefix: '[', suffix: '](url)', wrap: true }, |
| 198 |
{ label: '≡', title: 'Bullet List', line: '- ' }, |
| 199 |
{ label: '1.', title: 'Ordered List', line: '1. ' }, |
| 200 |
{ label: '❝', title: 'Blockquote', line: '> ' }, |
| 201 |
{ label: '━', title: 'Horizontal Rule', insert: '\n---\n' } |
| 202 |
]; |
| 203 |
|
| 204 |
tools.forEach(function (tool, idx) { |
| 205 |
if (idx === 3 || idx === 6 || idx === 12) { |
| 206 |
var sep = document.createElement('div'); |
| 207 |
sep.className = 'bkbg-mdp-toolbar-sep'; |
| 208 |
toolbar.appendChild(sep); |
| 209 |
} |
| 210 |
var btn = document.createElement('button'); |
| 211 |
btn.className = 'bkbg-mdp-tool-btn'; |
| 212 |
btn.style.color = a.toolbarColor; |
| 213 |
btn.textContent = tool.label; |
| 214 |
btn.title = tool.title; |
| 215 |
btn.addEventListener('click', function () { |
| 216 |
var ta = editorEl; |
| 217 |
var start = ta.selectionStart, end = ta.selectionEnd; |
| 218 |
var sel = ta.value.slice(start, end); |
| 219 |
var before = ta.value.slice(0, start); |
| 220 |
var after = ta.value.slice(end); |
| 221 |
var newVal, newCaret; |
| 222 |
|
| 223 |
if (tool.insert) { |
| 224 |
newVal = before + tool.insert + after; |
| 225 |
newCaret = start + tool.insert.length; |
| 226 |
} else if (tool.wrap) { |
| 227 |
var replacement = tool.prefix + (sel || 'text') + tool.suffix; |
| 228 |
newVal = before + replacement + after; |
| 229 |
newCaret = start + replacement.length; |
| 230 |
} else if (tool.line) { |
| 231 |
var lineStart = before.lastIndexOf('\n') + 1; |
| 232 |
var lineBefore = ta.value.slice(0, lineStart); |
| 233 |
var lineContent = ta.value.slice(lineStart); |
| 234 |
newVal = lineBefore + tool.line + lineContent; |
| 235 |
newCaret = lineStart + tool.line.length + (end - lineStart); |
| 236 |
} |
| 237 |
|
| 238 |
ta.value = newVal; |
| 239 |
ta.selectionStart = ta.selectionEnd = newCaret; |
| 240 |
ta.focus(); |
| 241 |
ta.dispatchEvent(new Event('input')); |
| 242 |
}); |
| 243 |
toolbar.appendChild(btn); |
| 244 |
}); |
| 245 |
|
| 246 |
/* Copy buttons */ |
| 247 |
if (a.showCopyBtn) { |
| 248 |
var tbRight = document.createElement('div'); |
| 249 |
tbRight.className = 'bkbg-mdp-toolbar-right'; |
| 250 |
toolbar.appendChild(tbRight); |
| 251 |
|
| 252 |
var copyMdBtn = document.createElement('button'); |
| 253 |
copyMdBtn.className = 'bkbg-mdp-copy-btn'; |
| 254 |
copyMdBtn.style.background = a.accentColor; |
| 255 |
copyMdBtn.textContent = 'Copy MD'; |
| 256 |
copyMdBtn.addEventListener('click', function () { |
| 257 |
navigator.clipboard && navigator.clipboard.writeText(editorEl.value).then(function () { |
| 258 |
copyMdBtn.textContent = '✓ Copied'; |
| 259 |
setTimeout(function () { copyMdBtn.textContent = 'Copy MD'; }, 1500); |
| 260 |
}); |
| 261 |
}); |
| 262 |
|
| 263 |
var copyHtmlBtn = document.createElement('button'); |
| 264 |
copyHtmlBtn.className = 'bkbg-mdp-copy-btn'; |
| 265 |
copyHtmlBtn.style.background = '#64748b'; |
| 266 |
copyHtmlBtn.textContent = 'Copy HTML'; |
| 267 |
copyHtmlBtn.addEventListener('click', function () { |
| 268 |
navigator.clipboard && navigator.clipboard.writeText(previewEl.innerHTML).then(function () { |
| 269 |
copyHtmlBtn.textContent = '✓ Copied'; |
| 270 |
setTimeout(function () { copyHtmlBtn.textContent = 'Copy HTML'; }, 1500); |
| 271 |
}); |
| 272 |
}); |
| 273 |
|
| 274 |
tbRight.appendChild(copyMdBtn); |
| 275 |
tbRight.appendChild(copyHtmlBtn); |
| 276 |
} |
| 277 |
} |
| 278 |
|
| 279 |
/* Split */ |
| 280 |
var split = document.createElement('div'); |
| 281 |
split.className = 'bkbg-mdp-split'; |
| 282 |
split.style.height = a.editorHeight + 'px'; |
| 283 |
container.appendChild(split); |
| 284 |
|
| 285 |
/* Editor pane */ |
| 286 |
var editorPane = document.createElement('div'); |
| 287 |
editorPane.className = 'bkbg-mdp-pane'; |
| 288 |
editorPane.style.cssText = 'background:' + a.editorBg + ';border-right:1px solid rgba(255,255,255,0.08);'; |
| 289 |
split.appendChild(editorPane); |
| 290 |
|
| 291 |
var editorLabel = document.createElement('div'); |
| 292 |
editorLabel.className = 'bkbg-mdp-pane-label'; |
| 293 |
editorLabel.style.cssText = 'color:' + a.toolbarColor + ';background:' + a.toolbarBg + ';border-color:rgba(255,255,255,0.08);'; |
| 294 |
editorLabel.textContent = 'Markdown'; |
| 295 |
editorPane.appendChild(editorLabel); |
| 296 |
|
| 297 |
var editorEl = document.createElement('textarea'); |
| 298 |
editorEl.className = 'bkbg-mdp-editor'; |
| 299 |
editorEl.style.cssText = 'background:' + a.editorBg + ';color:' + a.editorColor + ';'; |
| 300 |
editorEl.value = a.defaultContent; |
| 301 |
editorEl.spellcheck = false; |
| 302 |
editorPane.appendChild(editorEl); |
| 303 |
|
| 304 |
/* Preview pane */ |
| 305 |
var previewPane = document.createElement('div'); |
| 306 |
previewPane.className = 'bkbg-mdp-pane'; |
| 307 |
previewPane.style.background = a.previewBg; |
| 308 |
split.appendChild(previewPane); |
| 309 |
|
| 310 |
var previewLabel = document.createElement('div'); |
| 311 |
previewLabel.className = 'bkbg-mdp-pane-label'; |
| 312 |
previewLabel.style.cssText = 'color:#6b7280;border-color:' + a.borderColor + ';background:#f9fafb;'; |
| 313 |
previewLabel.textContent = 'Preview'; |
| 314 |
previewPane.appendChild(previewLabel); |
| 315 |
|
| 316 |
var previewEl = document.createElement('div'); |
| 317 |
previewEl.className = 'bkbg-mdp-preview-pane'; |
| 318 |
previewEl.style.color = a.previewColor; |
| 319 |
previewPane.appendChild(previewEl); |
| 320 |
|
| 321 |
/* Status bar */ |
| 322 |
if (a.showWordCount) { |
| 323 |
var statusBar = document.createElement('div'); |
| 324 |
statusBar.className = 'bkbg-mdp-statusbar'; |
| 325 |
statusBar.style.cssText = 'background:' + a.toolbarBg + ';color:' + a.toolbarColor + ';border-color:rgba(255,255,255,0.08);'; |
| 326 |
var wordCountEl = document.createElement('span'); |
| 327 |
statusBar.appendChild(wordCountEl); |
| 328 |
var charCountEl = document.createElement('span'); |
| 329 |
statusBar.appendChild(charCountEl); |
| 330 |
container.appendChild(statusBar); |
| 331 |
} |
| 332 |
|
| 333 |
function updateWordCount(text) { |
| 334 |
if (!a.showWordCount) return; |
| 335 |
var words = text.trim() ? text.trim().split(/\s+/).length : 0; |
| 336 |
wordCountEl.textContent = words + ' words'; |
| 337 |
charCountEl.textContent = text.length + ' chars'; |
| 338 |
} |
| 339 |
|
| 340 |
function render() { |
| 341 |
previewEl.innerHTML = parseMarkdown(editorEl.value); |
| 342 |
updateWordCount(editorEl.value); |
| 343 |
} |
| 344 |
|
| 345 |
editorEl.addEventListener('input', function () { render(); }); |
| 346 |
|
| 347 |
/* Tab key support */ |
| 348 |
editorEl.addEventListener('keydown', function (e) { |
| 349 |
if (e.key === 'Tab') { |
| 350 |
e.preventDefault(); |
| 351 |
var start = editorEl.selectionStart, end = editorEl.selectionEnd; |
| 352 |
editorEl.value = editorEl.value.slice(0, start) + ' ' + editorEl.value.slice(end); |
| 353 |
editorEl.selectionStart = editorEl.selectionEnd = start + 2; |
| 354 |
} |
| 355 |
}); |
| 356 |
|
| 357 |
/* Sync scroll */ |
| 358 |
if (a.syncScroll) { |
| 359 |
editorEl.addEventListener('scroll', function () { |
| 360 |
var pct = editorEl.scrollTop / (editorEl.scrollHeight - editorEl.clientHeight || 1); |
| 361 |
previewEl.scrollTop = pct * (previewEl.scrollHeight - previewEl.clientHeight); |
| 362 |
}); |
| 363 |
} |
| 364 |
|
| 365 |
render(); |
| 366 |
} |
| 367 |
|
| 368 |
function init() { |
| 369 |
document.querySelectorAll('.bkbg-mdp-app').forEach(initBlock); |
| 370 |
} |
| 371 |
|
| 372 |
if (document.readyState === 'loading') { |
| 373 |
document.addEventListener('DOMContentLoaded', init); |
| 374 |
} else { |
| 375 |
init(); |
| 376 |
} |
| 377 |
})(); |
| 378 |
|