Hexo的Butterfly主题带有一些特有的、非Markdown语法的外挂标签,作为一种写作语法扩展非常好用。然而在使用tabs标签时,遇到了内部代码块高度限制失效的问题。


我在Butterfly的主题文件中设置了代码块的高度限制:

highlight_height_limit: 300 # unit: px

一般情况下,当代码块高度超过300px时便会隐藏超出范围的代码,并添加一个展开按钮,但这个特性在tabs标签下的非默认tab中失效。

审计了渲染代码后,我找到了处理这部分逻辑的代码,其位于主题目录下的source/js/main.js文件中的createEle函数:

const createEle = (lang, item, service) => {
      const fragment = document.createDocumentFragment()

      if (isShowTool) {
        const hlTools = document.createElement('div')
        hlTools.className = `highlight-tools ${highlightShrinkClass}`
        hlTools.innerHTML = highlightShrinkEle + lang + highlightCopyEle
        btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
        fragment.appendChild(hlTools)
      }

      if (highlightHeightLimit && item.offsetHeight > highlightHeightLimit + 30) {
        const ele = document.createElement('div')
        ele.className = 'code-expand-btn'
        ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
        btf.addEventListenerPjax(ele, 'click', expandCode)
        fragment.appendChild(ele)
      }

      if (service === 'hl') {
        item.insertBefore(fragment, item.firstChild)
      } else {
        item.parentNode.insertBefore(fragment, item)
      }
    }

由于display:none的元素其offsetHeight会变成0,导致无法进入函数中间的那个if语句,也就无法按我们的需求进行渲染。

而对于这个问题,早就有了现成的解决方案。

我们可以引入jQuery Actual插件,通过它来获取隐藏元素的实际高度:

在butterfly的主题文件中inject.head项下引入jquery.actual文件:

- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.actual/1.0.19/jquery.actual.min.js"></script>

该文件还依赖于:jQuery >= 1.2.3

然后修改themes/butterfly/source/js/main.js中的createEle函数:

--- a/themes/butterfly/source/js/main.js
+++ b/themes/butterfly/source/js/main.js
@@ -134,8 +134,9 @@ document.addEventListener('DOMContentLoaded', function () {
         btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
         fragment.appendChild(hlTools)
       }
 
-      if (highlightHeightLimit && item.offsetHeight > highlightHeightLimit + 30) {
+      if (highlightHeightLimit && $(item).actual('height') > highlightHeightLimit + 30) {
         const ele = document.createElement('div')
         ele.className = 'code-expand-btn'
         ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'

毕竟为了一处小问题引入一个新的东西有点臃肿,不愿意引入jQuery的话还可以用原生JS解决。阅读了jQuery Actual的代码后我发现它是通过将隐藏起来的块暂时修改为:visibility: hidden; display: block;来获取它的高度,这是因为visibility: hidden;状态虽然也是隐藏,但仍会占据页面空间。在取得其高度后再恢复原有的样式。于是我们可以手写这个逻辑,来修改themes/butterfly/source/js/main.js

--- a/themes/butterfly/source/js/main.js
+++ b/themes/butterfly/source/js/main.js
@@ -124,6 +124,39 @@
       this.classList.toggle('expand-done')
     }
 
+    const getActualHeight = function (item) {
+      let tmp = []
+      let hidden = []
+      function fix() {
+      
+          let current = item
+          while (current !== document.body && current != null) {
+              if (window.getComputedStyle(current).display === 'none') {
+                  hidden.push(current)
+              }
+              current = current.parentNode
+          }
+          let style = 'visibility: hidden !important; display: block !important; '
+  
+          hidden.forEach(function (elem) {
+              var thisStyle = elem.getAttribute('style') || ''
+              tmp.push(thisStyle)
+              elem.setAttribute('style', thisStyle ? thisStyle + ';' + style : style)
+          })
+      }
+      function restore() {
+          hidden.forEach((elem, idx) => {
+              let _tmp = tmp[idx]
+              if( _tmp === '' ) elem.removeAttribute('style')
+              else elem.setAttribute('style', _tmp)
+          })
+      }
+      fix()
+      let height = item.offsetHeight
+      restore()
+      return height
+    }
+
     const createEle = (lang, item, service) => {
       const fragment = document.createDocumentFragment()
 
@@ -135,7 +168,7 @@
         fragment.appendChild(hlTools)
       }
 
-      if (highlightHeightLimit && item.offsetHeight > highlightHeightLimit + 30) {
+      if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
         const ele = document.createElement('div')
         ele.className = 'code-expand-btn'
         ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'

在作者的提示下才发现文档里已经有写:

  • 不适用于隐藏后的代码块( css 设置 display: none)

看来这是主题的预期行为。不过我个人认为让隐藏的代码块同样折叠起来更为合理。