自定义Hexo主题
很久很久以前就说过,要修改博客界面的一些细节样式,但是一直没有去做多少,只是在当前主题上修改了一点小细节(这也主要是因为这个主题基本符合我的要求,所以一直懒得继续修改)。
这里讲记录所有对Hexo
主题的自定义部分,当然,所有的操作都是基于Jacman
主题的,因此,有些操作可能并不适用于其它主题。
另外,主题安装日期为2015年3月18日,此后并未进行更新过,因此不保证代码和GitHub上最新版一致。也许某些下面提到的修复或者更改已经在上述GitHub链接中添加。
隐藏评论模块
有些时候,我们可能需要在某一个页面隐藏评论模块,但这个在原生主题中并没有提供,因此需要自己添加支持。至于方式,我们也可以采用类似默认配置的样式,在front-matter
(即标题以及日期等设置所在位置,以三个-
标记结束)中添加配置,然后在代码中可以使用page
属性来检查是否设置了相关配置。
比如这里我添加了一个属性名:disableshare
,bool
类型,用来标记是否需要隐藏评论模块,当设置了该属性并且其值为true
的时候,将不会再相应页面生成结果中添加评论模块,否则将会加入。这里需要注意的是,如果我们的.md
文件中本就没有写这个配置的话,将等价于属性值为false
,因此对于之前的一些文件,完全不需要修改配置部分。
与评论模块相关的源代码位于[jacman]/layout/_partial/post/footer.ejs
文件中,其中[jacman]
为你的主题目录。修改后代码如下:
<footer class="article-footer clearfix">
<%- partial('catetags') %>
<% if (!index){ %>
<div class="article-share" id="share">
<% if(theme.jiathis.enable){ %>
<div class="share-jiathis">
<%- partial('jiathis') %>
</div>
<!-- 此处首先检查设置,而不是直接输出 -->
<% } else if(!page.disableshare) { %>
<div data-url="<%- item.permalink %>" data-title="<% if (item.title){ %><%= item.title %> | <% } %><%= config.title %>" data-tsina="<%= theme.author.tsina %>" class="share clearfix">
</div>
<% } %>
</div>
<% } %>
<% if (index){ %>
<div class="comments-count">
<% if((config.disqus_shortname || theme.disqus_shortname) && !theme.duoshuo_shortname) { %>
<span></span>
<a href="<%- config.root %><%- item.path %>#disqus_thread" class="comments-count-link">Comments</a>
<% } else if(theme.duoshuo_shortname) { %>
<span></span>
<a href="<%- config.root %><%- item.path %>#comments" class="ds-thread-count comments-count-link" data-thread-key="<%- item.path %>" data-count-type="comments"> </a>
<% } %>
</div>
<% } %>
</footer>
最后别忘了在需要的页面添加配置disableshare: true
。
数学公式
在Jacman
主题中,已经内置了数学公式的支持,因此就不用我们自己去手动修改源码了。相关文件位置为[jacman]/layout/_partial/mathjax.ejs
,其中[jacman]
表示你的主题目录,这个文件默认类容大概如下:
<!-- mathjax config similar to math.stackexchange -->
<% if (page.mathjax){ %>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
processEscapes: true
}
});
</script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
}
});
</script>
<script type="text/x-mathjax-config">
MathJax.Hub.Queue(function() {
var all = MathJax.Hub.getAllJax(), i;
for(i=0; i < all.length; i += 1) {
all[i].SourceElement().parentNode.className += ' has-jax';
}
});
</script>
<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>
<% } %>
这是默认的类容,而我更愿意使用AsciiMath语法来书写数学公式,具体来说,就是使用两个`
符号包围数学公式的字符串就可以了,而公式语法形式也非常类似与平时在代码中的样式,因此上手相对要快一些。例如,下面的内容:
`x = (-b +- sqrt(b^2-4ac))/(2a) .`
将会被转换成如下样式:
`x = (-b +- sqrt(b^2-4ac))/(2a) .`
这里使用了MathJax来解析数学公式,有关这个JS库的使用方法,可以参见它的文档。需要注意的是,由于我们是在Markdown
文档中使用AsciiMath
语法,因此需要注意代码区块与数学公式区块之间的区别,因为他们使用了同一种字符来标记(当然,MathJax
中可以配置数学公式区块的标记字符)。
而还有一些细节,可能关乎到具体你使用的Markdown
解析器,就不多说了。
下面是修改后的mathjax.ejs
文件内容,主要是更换了JS文件引用,删除了一些无用的配置代码:
<!-- custom mathjax config -->
<% if (page.mathjax){ %>
<script type="text/x-mathjax-config">
MathJax.Hub.Queue(function() {
var all = MathJax.Hub.getAllJax(), i;
for(i=0; i < all.length; i += 1) {
all[i].SourceElement().parentNode.className += ' has-jax';
}
});
</script>
<script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=AM_CHTML">
</script>
<% } %>
而最后,需要记得在有数学公式的页面,front-matter
中加入mathjax: true
。
修复close_aside
配置无效的问题
在这一版本中,有一个配置close_aside
用于设置当进入文章阅读页面的时候,时候关闭右边的侧栏(侧栏中包括有标签、分类、友链等等小部件),从而提供更好的阅读体验。但是当实际配置的时候,即使在_config.yml
文件中设置close_aside: true
也没有作用,进入内容详情页,右边侧栏依然默认是展开的,之前就说要看看这个问题所在,但是也一直没有去查看原因,今天也算是一起解决了。
因为那个侧边栏上本来就有个汉堡按钮来控制边栏的隐藏与否,因此很容易想到在源代码的某个地方,肯定提供了JS控制方法,所以最简单的方法就是找到这些方法所在位置,并在后面按照主题中的配置来决定时候执行隐藏边栏的方法。
Jacman
源代码的组织还是很具有结构性的,从主题源代码根目录下layout
目录中可以找到一个layout.ejs
文件,这个文件用于控制每个页面布局,包括首页以及每个文章详情页,在其中我们可以找到这样的一个调用:
<%- partial('_partial/after_footer') %>
然后在_partial
目录下可以看到有个after_footer.ejs
文件,打开就可以看到所有的JavaScript
引用以及代码了。其中有个<script>
元素内容如下:
$(document).ready(function(){
$('.navbar').click(function(){
$('header nav').toggleClass('shownav');
});
var myWidth = 0;
function getSize(){
if( typeof( window.innerWidth ) == 'number' ) {
myWidth = window.innerWidth;
} else if( document.documentElement && document.documentElement.clientWidth) {
myWidth = document.documentElement.clientWidth;
};
};
var m = $('#main'),
a = $('#asidepart'),
c = $('.closeaside'),
o = $('.openaside');
$(window).resize(function(){
getSize();
if (myWidth >= 1024) {
$('header nav').removeClass('shownav');
}else
{
m.removeClass('moveMain');
a.css('display', 'block').removeClass('fadeOut');
o.css('display', 'none');
<% if( is_post()&&(page.toc !== false) && theme.toc.aside){ %>
$('#toc.toc-aside').css('display', 'none');
<% } %>
}
});
c.click(function(){
a.addClass('fadeOut').css('display', 'none');
o.css('display', 'block').addClass('fadeIn');
m.addClass('moveMain');
});
o.click(function(){
o.css('display', 'none').removeClass('beforeFadeIn');
a.css('display', 'block').removeClass('fadeOut').addClass('fadeIn');
m.removeClass('moveMain');
});
$(window).scroll(function(){
o.css("top",Math.max(80,260-$(this).scrollTop()));
});
});
主要控制代码就在c.click()
以及o.click()
中了,稍作修改,就可以实现我们需要的效果了;同时也顺便对$(window).resize()
中的不完善之处稍作修改。最终主要代码如下:
// ...
var m = $('#main'),
a = $('#asidepart'),
c = $('.closeaside'),
o = $('.openaside');
<% if (is_post() && theme.close_aside){ %>
var is_open = false;
<% }else{ %>
var is_open = true;
<% } %>
function closeAside(){
a.addClass('fadeOut').css('display', 'none');
o.css('display', 'block').addClass('fadeIn');
m.addClass('moveMain');
is_open = false;
};
function openAside(){
o.css('display', 'none').removeClass('beforeFadeIn');
a.css('display', 'block').removeClass('fadeOut').addClass('fadeIn');
m.removeClass('moveMain');
is_open = true;
}
$(window).resize(function(){
getSize();
if (myWidth >= 1024) {
$('header nav').removeClass('shownav');
if (is_open) {
openAside();
} else {
closeAside();
<% if( is_post()&&(page.toc !== false) && theme.toc.aside){ %>
$('#toc.toc-aside').css('display', 'block').addClass('fadeIn');
<% } %>
}
}else
{
m.removeClass('moveMain');
a.css('display', 'block').removeClass('fadeOut');
o.css('display', 'none');
<% if( is_post()&&(page.toc !== false) && theme.toc.aside){ %>
$('#toc.toc-aside').css('display', 'none');
<% } %>
}
});
c.click(function(){
closeAside();
});
o.click(function(){
openAside();
});
<% if (is_post() && theme.close_aside){ %>
getSize();
if (myWidth >= 1024) {
closeAside();
<% if( is_post()&&(page.toc !== false) && theme.toc.aside){ %>
$('#toc.toc-aside').css('display', 'block').addClass('fadeIn');
<% } %>
}
<% } %>
// ...
中间曾去找过判断当前页面是否是首页的方法,但是几种方式均代价太大,需要修改的地方也不少,直到偶然看到上面有一句if (is_post() && ...)
语句调用,猜测这个is_post()
方法便是判断当前页面是否为详情页(layout
属性为post
),于是也照着用,运行良好,可以保证只在详情页才会自动关闭边栏。
最后顺便修改了一下开关侧边栏那个按钮点击时的行为,默认情况那个按钮是个<a>
元素,并且其href
属性为#
,因此点击时候会回到页面顶部;将href
值改为javascript:void(0)
就好了。
代码高亮
Hexo
的一切都不错,但是唯独这个代码高亮做的总是不尽如人意,在Hexo
(而非其主题)的配置文件中,有一个highlight
配置,可以通过enable
属性配置开关,当设置enable: true
之后,就可以启用Hexo
的默认代码高亮行为,这是一个预处理行为,也就是在编译时期就已经确定了每个被显示的字符(串)的颜色,而无需再运行时在进行分析处理。
但是很不幸的一点是,Hexo
中使用的默认分析器没有做的足够好,很多时候都会出现不完整甚至错误的高亮行为,而更为棘手的一点是,这个用于处理代码高亮的模块,好像被内置于Hexo
的默认Markdown
编译器中了,外部可以更改的仅有几种简单的颜色属性;而在翻遍主题源码以及Hexo官网文档无果后,只能选择在运行时使用JS方式处理高亮。
既然决定使用JS进行高亮处理,那么自然也就不用使用内置的行为了,因为如果启用了内置行为,那么我们看起来一行行的代码,其实不知道被分成了多少个<p>
、<span>
等进行组织;这里就有一点很幸运了,当关闭内置处理之后,代码块会使用<pre><code>...</code></pre>
处理,而这就很符合一般代码高亮插件的要求了。而如果我们使用下面的方式来表示代码块:
``` language-type
// ...
```
那么会自动转换成如下的HTML代码:
<pre><code class="language-type">...</code></pre>
这种处理方式,刚好符合highlight.js要求。而在这里,我也是决定使用highlight.js
来进行高亮处理。
有关highlight.js的主题样式,可以在这个Demo站点查看。
为了方便,也是为了能尽可能的利用浏览器缓存进行加速,我是选择从网上寻找一个提供highlight.js的CDN服务器。我目前使用的是这个站点里面给出的地址。
在选择需要处理的语言以及代码块的主题样式之后,我们就可以将对应的css
以及js
文件引用分别加入到布局模板中了。其中在[jacman]/layout/_partial/head.ejs
添加需要的CSS样式文件引用(根据你需要的主题设置):
<link rel="stylesheet" href="//cdn.bootcss.com/highlight.js/9.1.0/styles/hopscotch.min.css">
然后在<body>
元素结尾的地方加入需要的JS引用,如上面所述,在[jacman]/layout/_partial/after_footer.ejs
中修改:
<script src="//cdn.bootcss.com/highlight.js/9.1.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
多说一句,这里我们可以看到链接中有个版本号,所以当
highlight.js
更新之后,只要CDN服务器已经同步更新,我们也可以直接更新相关文件引用。
同样,这里可以根据需要,选择更专门化的js文件。
经过测试,在主动标示语言类型的时候,需要使用小写方式,例如JavaScript应该写成javascript。
特殊处理
由于Jacman
默认生成的代码块样式与高亮插件有点冲突,可能需要自己手动调整一下,例如在<head>
元素中手动重写一遍高亮样式,并使用!important
来保证不会被覆盖。
下面记录下我修改的部分样式(添加于head
标签最后面):
<style type="text/css">
pre {display: block; overflow: auto !important; background: #322931 !important; padding: 0.5em;}
pre code {display: inline-block !important; color: #b9b5b8 !important;}
</style>
这里,为pre
标签设置了overflow
属性,并为pre
中的code
标签设置了display
样式,以保证代码块中的每一行被正确显示到一行之中,当页面宽度过窄时,代码块显示一个水平的滚动条而不是将代码中断换行。
至此,语法高亮基本解决(除了行号以外),如果有不完善的地方,以后再慢慢修正了。
显示标题编号
默认情况下,hexo
生成的文章详情页中,只有目录部分会显示标题编号,而到了正文中,每个标题却又如同一般Markdown
渲染器那样,只显示个标题了事,并没有给每个标题编号,如1.1
、3.2.4
等。一开始以为目录中的标题编号是主题自己做的,结果翻到了对应源码位置,发现又是hexo
自带了个toc
方法,这个方法的第二个参数就是设置是否生成标题编号的,也就是说,这个方法又是一个集成到系统内部的,没有暴露出太多细节,这不得不说是一种遗憾。
那没办法,只能再次通过js来在运行时动态设置了。通过查看Jacman
主题生成的详情页的默认结构,可以发现文章正文全部包含在一个class为.article-content
的div
元素中,每个标题对应的标签(h1
、h2
、…)都是该元素的直接子元素,并且内容结构都是类似的:一个长宽都为0的<a>
标签(不知道有什么作用)、以及实际的标题文本内容。因此我们可以很容易使用JQuery
来操作修改。
需要注意的是,我们一般并不会同时使用到完整的六种标题,大多数情况下就是使用两种或三种,有时候甚至只有一种,因此我们不能简单的根据标题分类来给标题编号(比如一个二级标题就一定编号为*.1
、*.2
等);而在特殊情况下我们有可能会跳过某一级标题(比如只使用二级、三级、五级标题),因此确定了起点标题(比如为二级标题)之后,我们也不能简单的根据当前标题(比如五级标题)与起点标题的级数差来确定编号中的分级。
下面给出这里使用的js代码,原理很简单,就是先找出每一级标题的数量,如果不为0(即此类型标题存在),那就在数组相应位置标记一下该级别标题的出现顺序。比如在一个只含有二级、三级、四级标题的列表中,二级标题出现顺序就为1,相应的,四级标题顺序为3,这样我们就可以确定每一级标题的编号中需要的分级数量了(即有多少个点.
分割)。
$(document).ready(function(){
var $article = $(".article-content");
var count = [0, 0, 0, 0, 0, 0];
var mark = [0, 0, 0, 0, 0, 0];
(function setMark(){
var hTag = ["h1", "h2", "h3", "h4", "h5", "h6"];
var index = 0;
for (var i = 0; i < 6; ++i) {
if ($article.children(hTag[i]).length > 0) {
++index;
mark[i] = index;
}
}
})();
$article.children(":header").each(function(){
var t = $(this);
var pos = 0;
switch (t[0].tagName) {
case "H1": pos = 0; break;
case "H2": pos = 1; break;
case "H3": pos = 2; break;
case "H4": pos = 3; break;
case "H5": pos = 4; break;
case "H6": pos = 5; break;
}
var len = mark[pos];
if (len < 6) { count[len] = 0; }
count[len - 1]++;
var listStr = count[0] + "";
for (var i = 1; i < len; ++i) { listStr += "." + count[i]; }
listStr += " ";
t.html(listStr + t.html());
});
});
这里我并未没有考虑比如第一个遇到的标题类型并不是最顶级的标题这样的情况,因为在一般博客中,几乎很少有这种现象,因此就不做更多考虑了。如果你需要处理这种情况的话,可以自行修改代码。
而最后需要注意的是,因为我们只需要在文章详情页使用这段js代码,因此记得使用is_post()
方法判断一下是否插入代码。至于代码插入位置,依然是那个after_footer.ejs
文件了。