/* article.js —— 文章库三栏阅读交互 * 功能:三栏(标题栏 / 段落栏 / 正文)宽度拖拽调节、分区栏与段落栏显示/隐藏; * Markdown 正文 CDN 懒加载渲染、正文图片路径前缀补全、段落目录生成与平滑跳转。 */ (function (global) { 'use strict'; function $(sel, root) { return (root || document).querySelector(sel); } function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } var app = $('.js-art-app'); var frame = $('.js-art-frame'); if (!app || !frame) return; var BODY_MIN = 22; // 正文列最小占比(%) var KEY = 'hp-art-layout'; var MEDIA_SMALL = '(max-width: 860px)'; /* ---------- 布局状态(宽度 / 显隐) ---------- */ function loadState() { var isList = global.HP_ART_DATA && global.HP_ART_DATA.listMode ? true : false; var st = { w1: 15, w2: 15, t1: true, t2: !isList }; // 列表页默认收起段落栏 try { var saved = JSON.parse(localStorage.getItem(KEY) || 'null'); if (saved) { st.w1 = clampPct(saved.w1, 6, 60); st.w2 = clampPct(saved.w2, 6, 60); if (typeof saved.t1 === 'boolean') st.t1 = saved.t1; if (typeof saved.t2 === 'boolean') st.t2 = saved.t2; } else if (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) { // 窄屏首次进入:默认只显示正文,两栏以浮层形式随时唤出 st.t1 = false; st.t2 = false; } } catch (e) { /* ignore */ } return st; } function saveState(st) { try { localStorage.setItem(KEY, JSON.stringify(st)); } catch (e) { /* ignore */ } } function clampPct(v, min, max) { v = parseFloat(v); if (isNaN(v)) return min; return Math.min(max, Math.max(min, v)); } var st = loadState(); function isSmall() { return (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) ? true : false; } function applyLayout() { frame.style.setProperty('--w-title', st.w1 + '%'); frame.style.setProperty('--w-toc', st.w2 + '%'); var small = isSmall(); var tTitle = $('.js-col-title'); var tToc = $('.js-col-toc'); var gTitle = $('.js-gutter[data-var="--w-title"]'); var gToc = $('.js-gutter[data-var="--w-toc"]'); if (!small) { if (tTitle) { tTitle.classList.toggle('art-col-hidden', !st.t1); } if (tToc) { tToc.classList.toggle('art-col-hidden', !st.t2); } if (gTitle) gTitle.style.display = st.t1 ? '' : 'none'; if (gToc) gToc.style.display = st.t2 ? '' : 'none'; } else { // 窄屏:默认隐藏,靠开关以浮层方式显示 if (tTitle) tTitle.classList.toggle('art-col-show-mobile', !!st.t1); if (tToc) tToc.classList.toggle('art-col-show-mobile', !!st.t2); if (gTitle) gTitle.style.display = 'none'; if (gToc) gToc.style.display = 'none'; } updateToggleBtns(); } function setColVisible(col, visible) { if (col === 'title') st.t1 = visible; if (col === 'toc') st.t2 = visible; applyLayout(); saveState(st); } function updateToggleBtns() { $$('.js-art-toggle').forEach(function (b) { var c = b.getAttribute('data-col'); var visible = (c === 'title') ? st.t1 : st.t2; b.classList.toggle('active', visible); var inSmall = isSmall(); b.title = (inSmall ? (visible ? '收起' : '展开') + ((c === 'title') ? '文章标题栏' : '段落目录') : '显示 / 隐藏' + ((c === 'title') ? '文章标题栏' : '段落目录')); }); } function bindToggles() { document.addEventListener('click', function (ev) { var b = ev.target && ev.target.closest ? ev.target.closest('.js-art-toggle') : null; if (!b) return; var c = b.getAttribute('data-col'); if (c !== 'title' && c !== 'toc') return; var cur = (c === 'title') ? st.t1 : st.t2; setColVisible(c, !cur); }); } /* ---------- 分隔条拖拽 ---------- */ function bindGutters() { $$('.js-gutter', frame).forEach(function (g) { g.addEventListener('mousedown', function (ev) { ev.preventDefault(); var colVar = g.getAttribute('data-var'); // --w-title / --w-toc var isTitle = colVar.indexOf('title') !== -1; var startX = ev.clientX; var frameW = frame.getBoundingClientRect().width || 1; var startW = isTitle ? st.w1 : st.w2; var other = isTitle ? st.w2 : st.w1; var moved = false; function onMove(e) { var dx = e.clientX - startX; var pct = startW + (dx / frameW) * 100; var max = 100 - BODY_MIN - other; // 保证正文列至少 BODY_MIN% pct = clampPct(pct, 6, Math.max(6, max)); if (Math.abs(pct - startW) > 0.2) moved = true; if (isTitle) { st.w1 = pct; } else { st.w2 = pct; } frame.style.setProperty(colVar, pct + '%'); g.classList.add('dragging'); } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); g.classList.remove('dragging'); if (moved) { if (isTitle) { st.w1 = clampPct(st.w1, 6, Math.max(6, 100 - BODY_MIN - st.w2)); } else { st.w2 = clampPct(st.w2, 6, Math.max(6, 100 - BODY_MIN - st.w1)); } saveState(st); } } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); }); } /* ---------- Markdown 库 CDN 懒加载(marked v4) ---------- */ var MARKED_URLS = [ 'https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js', 'https://unpkg.com/marked@4.3.0/marked.min.js' ]; var markedPromise = null; function loadMarked() { if (global.marked) return Promise.resolve(global.marked); if (markedPromise) return markedPromise; markedPromise = new Promise(function (resolve) { var tryLoad = function (i) { if (global.marked) { resolve(global.marked); return; } if (i >= MARKED_URLS.length) { resolve(null); return; } var s = document.createElement('script'); s.src = MARKED_URLS[i]; s.onload = function () { resolve(global.marked || null); }; s.onerror = function () { tryLoad(i + 1); }; document.head.appendChild(s); }; tryLoad(0); }); return markedPromise; } /* ---------- 正文渲染与目录 ---------- */ var rawEl = document.getElementById('artRaw'); var mdBox = document.getElementById('artMd'); var tocNav = $('.js-toc-nav'); var tocCol = $('.js-col-toc'); function fixRelativeUrls() { // 正文图片/链接可能使用 data/articles/img/… 站点相对根路径(相对站点根), // 页面位于 article/ 子目录,渲染后需补 ../ 前缀才能正确指向 data/articles/img/。 if (!mdBox) return; var absPrefixes = ['#', 'http:', 'https:', '//', 'mailto:', 'tel:', 'data:', '/', '../']; function apply(attr) { mdBox.querySelectorAll('img, a').forEach(function (el) { var v = el.getAttribute(attr); if (!v) return; var t = v.trim(); for (var i = 0; i < absPrefixes.length; i++) { if (t.indexOf(absPrefixes[i]) === 0) return; } // 纯相对路径:视为相对站点根 → 相对当前 article/ 目录需 ../ el.setAttribute(attr, '../' + v); }); } apply('src'); apply('href'); } function renderError() { var tpl = document.getElementById('artErrTpl'); if (mdBox && tpl) mdBox.innerHTML = tpl.textContent; } function buildToc() { if (!mdBox || !tocNav) return; var heads = $$('#artMd h1, #artMd h2, #artMd h3, #artMd h4, #artMd h5'); tocNav.innerHTML = ''; if (!heads.length) { tocNav.innerHTML = '
本文暂无段落标题
'; return; } var frag = document.createDocumentFragment(); heads.forEach(function (h, i) { var id = 'art-sec-' + i; h.id = id; var lv = parseInt(h.tagName.charAt(1), 10) || 1; var level = Math.min(4, Math.max(1, lv)); var a = document.createElement('a'); a.href = '#' + id; a.className = 'art-toc-link lv' + level; a.textContent = h.textContent; a.addEventListener('click', function (e) { e.preventDefault(); var target = document.getElementById(id); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); frag.appendChild(a); }); tocNav.appendChild(frag); } function renderArticle() { if (!rawEl || !mdBox) { if (tocNav) tocNav.innerHTML = '选择左侧任意文章后,此处生成段落目录。
'; return; } loadMarked().then(function (marked) { if (!marked) { renderError(); return; } var md = rawEl.value || ''; var html = marked.parse(md, { gfm: true, breaks: false }); mdBox.innerHTML = html; fixRelativeUrls(); buildToc(); // 高亮标题栏当前项 var curId = (global.HP_ART_DATA && global.HP_ART_DATA.curId) ? global.HP_ART_DATA.curId : 0; if (curId) { var link = $('.js-art-nav a[href="?id=' + curId + '"]'); if (link) { $$('.js-art-nav a').forEach(function (a) { a.classList.remove('active'); }); link.classList.add('active'); } } }); } /* ---------- 初始化 ---------- */ function init() { applyLayout(); bindToggles(); bindGutters(); renderArticle(); // 窗口尺寸变化(跨断点时重新应用显隐策略) var resizeTimer = null; window.addEventListener('resize', function () { if (resizeTimer) clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { applyLayout(); }, 120); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(window);