/* 약관(Markdown) 편집기. * - 기본은 textarea: 사용자가 직접 #, - 등을 입력할 수 있다. * - "/" 를 줄 맨 앞 또는 빈 공간 다음에 입력하면 슬래시 메뉴를 띄워 * 제목/내용/글머리/번호/토글/구분선/인용/코드 블록을 선택해 자동 삽입한다. * (사용자가 #, - 같은 기호를 외울 필요 없이 명령어로 입력 가능) * - 미리보기 탭에서 작은 markdown → HTML 렌더러로 결과를 보여 준다. */ (function () { 'use strict' var editor = document.getElementById('editor') var preview = document.getElementById('preview') var slashMenu = document.getElementById('slashMenu') var status = document.getElementById('status') var dirtyMark = document.getElementById('dirty-mark') var saveBtn = document.getElementById('saveBtn') var tabBtns = document.querySelectorAll('.tabBar .tabBtn') var visInstaller = document.getElementById('visInstaller') var visInstallerRp = document.getElementById('visInstallerRp') editor.value = INITIAL || '' var dirty = false function setDirty(v) { dirty = v dirtyMark.hidden = !v } // 토글이 바뀌어도 dirty 표시. 저장 시 함께 전송된다. if (visInstaller) visInstaller.addEventListener('change', function () { setDirty(true) }) if (visInstallerRp) visInstallerRp.addEventListener('change', function () { setDirty(true) }) // ─── markdown 미리 보기용 미니 렌더러 ──────────────────────────────── // 정식 markdown 파서는 아니지만, 본 편집기가 만들어 내는 형태(#, ##, ###, // - , 1. , > , ---, ``` , 토글 details) 정도는 충실히 처리한다. function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>') } function inline(s) { s = escHtml(s) // code `x` s = s.replace(/`([^`]+)`/g, '$1') // bold **x** s = s.replace(/\*\*([^*]+)\*\*/g, '$1') // italic *x* s = s.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, '$1$2') // links [text](url) — also auto-link bare http(s) s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') s = s.replace(/(^|[\s(])(https?:\/\/[^\s)]+)/g, function (m, p, u) { return p + '' + u + '' }) return s } function renderMd(src) { var lines = src.replace(/\r\n/g, '\n').split('\n') var out = [] var i = 0 var stackList = null // 'ul' | 'ol' | null function closeList() { if (stackList) { out.push(''); stackList = null } } while (i < lines.length) { var line = lines[i] // 코드 블록 ```lang var fence = /^```(\w*)\s*$/.exec(line) if (fence) { closeList() var code = [] i += 1 while (i < lines.length && !/^```\s*$/.test(lines[i])) { code.push(lines[i]); i += 1 } if (i < lines.length) i += 1 out.push('
' + escHtml(code.join('\n')) + '
') continue } // 토글 (자체 구문) :::toggle 제목 ... ::: var togStart = /^:::toggle\s+(.+)$/.exec(line) if (togStart) { closeList() var summary = togStart[1] var body = [] i += 1 while (i < lines.length && !/^:::\s*$/.test(lines[i])) { body.push(lines[i]); i += 1 } if (i < lines.length) i += 1 out.push('
' + inline(summary) + '' + renderMd(body.join('\n')) + '
') continue } // 헤딩 var h = /^(#{1,6})\s+(.*)$/.exec(line) if (h) { closeList() var level = h[1].length out.push('' + inline(h[2]) + '') i += 1; continue } // hr if (/^---+\s*$/.test(line)) { closeList() out.push('
'); i += 1; continue } // 인용 > if (/^>\s?/.test(line)) { closeList() var q = [] while (i < lines.length && /^>\s?/.test(lines[i])) { q.push(lines[i].replace(/^>\s?/, '')); i += 1 } out.push('
' + renderMd(q.join('\n')) + '
') continue } // 번호 목록 var ol = /^\s*\d+\.\s+(.*)$/.exec(line) if (ol) { if (stackList !== 'ol') { closeList(); out.push('
    '); stackList = 'ol' } out.push('
  1. ' + inline(ol[1]) + '
  2. ') i += 1; continue } // 불릿 var ul = /^\s*[-*]\s+(.*)$/.exec(line) if (ul) { if (stackList !== 'ul') { closeList(); out.push('