项目结构描述
Shiroi 魔改(typo)日志
(解耦合) 标签页
标签页用于显示标题和站点图标.
创建文件layout/_partial/label.ejs
.
标签页将实现使用icon.svg
为图标
标题可在站点配置文件_config.yml
或者主题配置文件_config.shiroi.yml
中配置(存在优先关系).
1 2 3 4 5 6 7 8 <!-- 标签页 --> <title> <%= page.title || config.title || theme.title %> </title> <% if (theme.favicon){ %> <%- favicon_tag(theme.favicon) %> <% } %>
在layout/layout.ejs
中注入配置
1 2 <!-- 标签页 --> <%- partial('_partial/label') %>
(解耦合) 字体
字体是全局主题使用的字体,继承了 typo 主题的配置
创建文件layout/_partial/font.ejs
.
1 2 3 4 5 <!-- 字体 --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
在layout/_partial/global.ejs
中注入配置
1 2 <!-- 字体 --> <%- partial('_partial/font.ejs') %>
(解耦合) 导航栏
导航栏是每个页面最上方的 top bar, 用于展示站点配置文件中的选项menu
.
创建文件layout/_partial/header.ejs
.
导航栏将实现使用icon.svg
为图标
左侧标题可在站点配置文件中配置.
右侧图标可以在icon/
文件夹中找到对应名字更换.
右侧图标后名字取决于主题配置文件的 menu 中的配置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!--(头部 top bar) Header 样式 --> <%- css('css/header.css') %> <header class="header"> <section class="header-container"> <a class="header-logo" href="<%- url_for('icon.svg') %>"><%= config.title %></a> <ul class="header-nav"> <% for (const name in theme.menu) { const iconName = name; %> <li> <a href="<%- url_for(theme.menu[name]) %>"> <img src="<%- url_for('icon/' + iconName + '.svg') %>" alt="<%= name %> icon" style="width:1.2em;height:1.2em;vertical-align:middle;"> <%= name %> </a> </li> <% } %> </ul> </section> </header>
在layout/layout.ejs
中注入配置
1 2 <!-- 导航栏 --> <%- partial('_partial/header') %>
创建样式文件source/css/header.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 .header {position : sticky;top : -48px ;background : inherit;margin : 0 ;padding : 64px 8vw 0 ;max-width : 100% ;width : 100% ;} .header-container { display : flex; justify-content : space-between; align-items : center; border-bottom : 1px solid var (--color-hr); } .header-logo { font-size : 2rem ; font-weight : 600 ; color : var (--font-color); margin-bottom : 16px ; } .header-nav { margin : 0 0 4px ; display : flex; flex-direction : row; flex-wrap : wrap; justify-content : flex-start; align-content : center; list-style : none; } .header-nav li { padding : 2px 0 ; margin-right : 24px ; } .header-nav li :last-of-type { margin-right : 0 ; } .header-nav li a { color : var (--font-color); } .header-nav li a img { margin-right : 0.3em ; vertical-align : middle; display : inline-block; } .header-nav li a :hover { text-decoration : underline; }
页脚
页脚是底部的版权注释, 可在主题配置文件中的选项copyright
修改相关描述.
typo 中已有layout/_partial/footer.ejs
文件,用于描述页脚的配置.
数学公式支持
如果你的markdown渲染器和作者一样为hexo-renderer-syzoj-renderer
;那么此开关不是必要的;
如果是其他渲染器,可能需要这个开关的支持。
在主题配置文件中添加开关
在layout/global.ejs
添加如下代码,以使得渲染器支持数学公式
1 2 3 <!-- 数学公式 --> <%- css('css/mathjax.css') %> <%- js('js/mathjax.js') %>
添加样式文件source/css/mathjax.css
使得渲染器渲染出字体为Times New Roman
。以及支持让公式在小屏幕下自动换行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .mathjax-formula { font-family : 'Times New Roman' , Times, serif; font-size : 1.05em ; line-height : 1.5 ; } .MathJax_Display { overflow-x : auto; overflow-y : hidden; width : 100% ; box-sizing : border-box; }
添加脚本文件source/js/mathjax.js
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (function ( ) { var script = document .createElement ('script' ); script.type = 'text/javascript' ; script.src = window .THEME_CONFIG .mathjax .src || 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js' ; script.async = true ; window .MathJax = { tex : { inlineMath : [['$' , '$' ], ['\\(' , '\\)' ]] } }; document .head .appendChild (script); })();
(文章主体) 去掉标题前的符号#
作者不喜欢标题符号的设计,注释掉上面source/css/post.css
中的以下代码即可:
1 2 3 4 5 6 7 8 9 10 11 .post-content h1 ::before ,.post-content h2 ::before ,.post-content h3 ::before ,.post-content h4 ::before ,.post-content h5 ::before ,.post-content h6 ::before { content : "⌗" ; padding-right : 10px ; color : var (--color-hashtag); font-weight : 600 ; }
(文章主体) 在无序列表前面加点
作者更习惯在无序列表前加点,修改source/css/root.css
中的全局样式,
注释掉原来的样式,替换成如下样式
1 2 3 4 5 6 7 8 9 10 11 .post-content ul { list-style : disc; padding-left : 2em ; } .post-content ul ul { list-style : circle; }
(文章可选配置) 标题自动编号
在配置文件(如 _config.typo.yml
或 themes/shiroi/_config.yml
)中添加开关
在 layout/_partial/head.ejs
的 </head>
前注入两个文件。
1 2 3 4 5 6 <!-- 标题编号 --> <% if (theme.heading_numbering || config.heading_numbering) { %> <%- css('css/heading-numbering.css') %> <%- js('js/heading-numbering.js') %> <% } %> </head>
新建一个文件 source/js/heading-numbering.js
,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 document .addEventListener ('DOMContentLoaded' , function ( ) { if (!document .querySelector ('.post' )) return ; const headings = document .querySelectorAll ('.post h2, .post h3, .post h4, .post h5, .post h6' ); const nums = [0 , 0 , 0 , 0 , 0 ]; headings.forEach (heading => { const level = parseInt (heading.tagName [1 ]) - 2 ; if (level < 0 ) return ; nums[level]++; for (let i = level + 1 ; i < nums.length ; i++) nums[i] = 0 ; const numbering = nums.slice (0 , level + 1 ).join ('.' ); heading.innerHTML = `<span class="heading-number">${numbering} </span>` + heading.innerHTML ; }); });
新建一个文件 source/css/heading-numbering.css
,内容如下:
1 2 3 4 5 6 .heading-number { color : #888 ; font-weight : normal; margin-right : 0.5em ; font-size : 0.95em ; }
(文章可选配置) 三线表样式
此功能用于将标准的 Markdown 表格渲染为学术论文中常见的三线表样式,以增强可读性和专业性。该功能可通过主题配置文件中的开关进行控制。
首先,在主题配置文件 _config.yml
中添加以下开关:
在 themes/shiroi/layout/_partial/head.ejs
中添加以下代码,以根据配置开关加载样式文件:
1 2 3 4 <!-- 三线表样式 --> <% if (theme.three_line_table) { %> <%- css('css/three-line-table.css') %> <% } %>
新建一个文件source/css/three-line-table.css
,内容如下。
为了避免样式污染用于代码块的表格(它们在渲染后也是 <table>
结构),
所有规则都使用了 :not(.hljs)
选择器进行限定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 .post-content table :not (.hljs ) { width : 100% ; border-collapse : collapse; border-top : 2px solid var (--color-post-content, #333 ); border-bottom : 2px solid var (--color-post-content, #333 ); margin : 1.5em 0 ; font-size : 0.95em ; } .post-content table :not (.hljs ) th ,.post-content table :not (.hljs ) td { border : none; padding : 0.75em 1em ; text-align : left; } .post-content table :not (.hljs ) thead th { border-bottom : 1px solid var (--color-post-content, #333 ); text-align : center; font-weight : 600 ; } .post-content table :not (.hljs ) tbody tr :nth-child (even) { background-color : var (--color-table-row, #f9f9f9 ); } .post-content table :not (.hljs ) caption { caption-side : bottom; text-align : center; font-size : 0.9em ; color : #888 ; margin-top : 0.5em ; }
(文章可选配置) 图片样式
此功能用于自动居中图片,并将其 alt
文本作为带编号的图注(例如"图1:xxx")显示。
在主题配置文件 _config.yaml
中添加以下开关:
1 2 3 4 5 image: enable: true center: true caption: true
修改布局文件来注入样式资源
在 layout/_partial/head.ejs
中注入样式:
1 2 3 4 <!-- 图片样式 --> <% if (theme.image && theme.image.enable) { %> <%- css('css/image-handler.css') %> <% } %>
在 layout/layout.ejs
的 <body>
标签内部添加配置注入:
1 2 3 4 5 6 <body <% if (theme.image && theme.image.enable) { %> data-image-center="<%= theme.image.center ? 'true' : 'false' %>" data-image-caption="<%= theme.image.caption ? 'true' : 'false' %>" <% } %> >
新建文件 source/css/image-handler.css
。
我们使用 display: table;
技巧让 <figure>
元素的宽度自适应其内容,
从而使 margin: auto;
能够生效,实现真正的居中。
1 2 3 4 5 6 7 8 9 10 11 12 13 .post-content .image-figure { display : table; margin-left : auto; margin-right : auto; } .post-content .image-figure figcaption { margin-top : 0.8em ; font-size : 0.9em ; color : #888 ; text-align : center; }
新建文件 source/js/image-handler.js
。
此脚本会查找文章中被单独包裹在 <p>
标签里的图片,
用一个带有 .image-figure
类的 <figure>
元素替换掉原来的 <p>
标签,
并根据配置添加图注。
通过 document.body.dataset 读取配置,完全解耦 JS 与模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 document .addEventListener ('DOMContentLoaded' , () => { const body = document .body ; const center = body.dataset .imageCenter === 'true' ; const caption = body.dataset .imageCaption === 'true' ; if (!center && !caption) return ; let imageCounter = 0 ; const content = document .querySelector ('.post-content' ); if (!content) return ; const images = content.querySelectorAll ('img' ); images.forEach (img => { const parent = img.parentNode ; if (parent.tagName === 'P' && parent.children .length === 1 ) { if (caption && img.alt ) { imageCounter++; const figure = document .createElement ('figure' ); figure.classList .add ('image-figure' ); const figcaption = document .createElement ('figcaption' ); figcaption.innerText = `图 ${imageCounter} : ${img.alt} ` ; figure.appendChild (img); figure.appendChild (figcaption); parent.parentNode .replaceChild (figure, parent); } else if (center) { parent.style .textAlign = 'center' ; } } }); });
(文章可选配置) 代码块样式升级
此功能旨在将默认的代码块替换为一个功能更丰富、样式更现代的组件。
它包含一个标题栏,用于显示代码语言和提供一键复制功能,并采用了仅保留关键分割线的极简设计。
最小化注释全局干扰样式
在source/css/post.css
中,注释掉如下代码。
功能开关与配置
在主题配置文件 _config.shiroi.yml
中添加以下配置,用于控制复制按钮的显示内容。
1 2 3 4 5 code_block: copy_button: icon/copy.svg
代码块逻辑集成
在 themes/shiroi/source/js/code-block.js
文件中,实现了如下功能
初始化 highlight.js
;
创建代码块头部;
添加复制功能;
主题切换功能;
此项目依赖 highlight.js
库,确保它已被引入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 if (typeof hljs !== 'undefined' ) { hljs.highlightAll (); } document .addEventListener ('DOMContentLoaded' , () => { const codeBlocks = document .querySelectorAll ('figure.highlight' ); const bodyDataset = document .body .dataset ; const copyButtonConfig = bodyDataset.codeBlockCopyButton ; const themeToggleEnable = bodyDataset.codeBlockThemeToggleEnable === 'true' ; const themeToggleConfig = { enable : themeToggleEnable, to_light_button : bodyDataset.codeBlockThemeToggleToLightButton , to_dark_button : bodyDataset.codeBlockThemeToggleToDarkButton , }; codeBlocks.forEach (codeBlock => { if (codeBlock.querySelector ('.code-block-header' )) return ; const table = codeBlock.querySelector ('table' ); if (table) { table.classList .add ('hljs' ); } const lang = Array .from (codeBlock.classList ).find (cls => cls !== 'highlight' ) || 'code' ; const createCodeBlockHeader = (langName ) => { const header = document .createElement ('div' ); header.className = 'code-block-header' ; const langSpan = document .createElement ('span' ); langSpan.className = 'code-lang-name' ; langSpan.innerText = langName; header.appendChild (langSpan); const actionsContainer = document .createElement ('div' ); actionsContainer.className = 'code-block-actions' ; header.appendChild (actionsContainer); return { header, actionsContainer }; }; const addCopyButton = (actionsContainer, codeBlock, copyButtonPath ) => { const container = document .createElement ('div' ); container.className = 'copy-btn-container' ; const copyButton = document .createElement ('button' ); copyButton.className = 'copy-btn' ; let originalContent; if (copyButtonPath) { const icon = document .createElement ('img' ); icon.src = '/' + copyButtonPath; copyButton.appendChild (icon); originalContent = icon.outerHTML ; } else { copyButton.innerText = 'copy' ; originalContent = 'copy' ; } container.appendChild (copyButton); actionsContainer.appendChild (container); copyButton.addEventListener ('click' , () => { const codeToCopy = codeBlock.querySelector ('.code' ).innerText ; navigator.clipboard .writeText (codeToCopy).then (() => { copyButton.innerText = 'copied!' ; setTimeout (() => { copyButton.innerHTML = originalContent; }, 3000 ); }).catch (err => { console .error ('Failed to copy text: ' , err); }); }); }; const addThemeToggleButton = (actionsContainer, codeBlock, toggleConfig ) => { if (!toggleConfig || !toggleConfig.enable ) return ; const lightButton = document .createElement ('button' ); lightButton.className = 'theme-toggle-btn light' ; const lightIcon = document .createElement ('img' ); lightIcon.src = '/' + toggleConfig.to_dark_button ; lightButton.appendChild (lightIcon); const darkButton = document .createElement ('button' ); darkButton.className = 'theme-toggle-btn dark' ; const darkIcon = document .createElement ('img' ); darkIcon.src = '/' + toggleConfig.to_light_button ; darkButton.appendChild (darkIcon); darkButton.style .display = 'inline-block' ; lightButton.style .display = 'none' ; actionsContainer.appendChild (lightButton); actionsContainer.appendChild (darkButton); darkButton.addEventListener ('click' , () => { const figure = darkButton.closest ('figure.highlight' ); if (figure) { figure.classList .remove ('theme-light' ); figure.classList .add ('theme-dark' ); } darkButton.style .display = 'none' ; lightButton.style .display = 'inline-block' ; }); lightButton.addEventListener ('click' , () => { const figure = lightButton.closest ('figure.highlight' ); if (figure) { figure.classList .remove ('theme-dark' ); figure.classList .add ('theme-light' ); } lightButton.style .display = 'none' ; darkButton.style .display = 'inline-block' ; }); }; const { header, actionsContainer } = createCodeBlockHeader (lang); codeBlock.prepend (header); if (copyButtonConfig) { addCopyButton (actionsContainer, codeBlock, copyButtonConfig); } if (themeToggleConfig.enable ) { addThemeToggleButton (actionsContainer, codeBlock, themeToggleConfig); codeBlock.classList .add ('theme-light' ); } }); });
CSS 样式集成
在 themes/shiroi/source/css/code-block-base.css
文件中集成所有基础样式。
这份样式表经过了多次迭代,拥有极高的优先级,能够覆盖主题中其他冲突的全局样式,确保最终效果的稳定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 figure .highlight { --code-bg : #f7f7f7 ; --header-bg : #f7f7f7 ; --table-bg : white; --gutter-bg : #f7f7f7 ; --border-color : #eee ; --lang-name-color : #555 ; border-radius : 8px ; margin : 1.5em 0 ; overflow : hidden; background-color : var (--code-bg); } figure .highlight .theme-dark { --code-bg : #282c34 ; --header-bg : #21252b ; --table-bg : #282c34 ; --gutter-bg : #21252b ; --border-color : #3a4049 ; --lang-name-color : #abb2bf ; } .code-block-header { display : flex; justify-content : space-between; align-items : center; padding : 0.5em 1em ; background-color : var (--header-bg); border-bottom : 1px solid var (--border-color); } .code-lang-name { font-size : 0.9em ; color : var (--lang-name-color); text-transform : capitalize; font-family : 'JetBrains Mono' , monospace; } .copy-btn-container .copy-btn { border : none; cursor : pointer; background-color : transparent; opacity : 0.6 ; } .copy-btn-container .copy-btn :hover { opacity : 1 ; } .copy-btn-container .copy-btn img ,.code-block-actions .theme-toggle-btn img { width : 16px ; height : 16px ; vertical-align : middle; } figure .highlight table .hljs { background-color : var (--table-bg); width : 100% ; margin : 0 ; border-spacing : 0 ; border-radius : 0 0 8px 8px ; } .post-content figure .highlight table .hljs td { border : none !important ; padding : 0 !important ; } .post-content figure .highlight table .hljs .gutter { background-color : var (--gutter-bg); padding : 1em !important ; border-right : 1px solid var (--border-color) !important ; } .post-content figure .highlight table .hljs .code { padding : 1em !important ; } .post-content figure .highlight table .hljs .gutter .line { color : var (--lang-name-color); opacity : 0.5 ; }
模板注入
为了让上述功能生效,需要在模板文件中引入对应的CSS和JS,并将配置通过 body
标签的 data-
属性注入。
在 themes/shiroi/layout/_partial/head.ejs
中添加:
1 2 3 4 5 6 7 <!-- 代码块基础样式 --> <%- css('css/code-block-base.css') %> <!-- Codeblock Theme Toggler CSS --> <% if (theme.code_block && theme.code_block.theme_toggle && theme.code_block.theme_toggle.enable) { %> <%- css('css/codeblock-theme-toggle.css') %> <% } %>
在 themes/shiroi/layout/layout.ejs
的 <body>
标签内部添加配置注入:
1 2 3 4 5 6 7 8 9 10 11 12 <body <% if (theme.image && theme.image.enable) { %> data-image-center="<%= theme.image.center ? 'true' : 'false' %>" data-image-caption="<%= theme.image.caption ? 'true' : 'false' %>" <% } %> <% if (theme.code_block) { %> data-code-block-copy-button="<%= theme.code_block.copy_button || '' %>" data-code-block-theme-toggle-enable="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.enable) ? 'true' : 'false' %>" data-code-block-theme-toggle-to-light-button="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.to_light_button) || '' %>" data-code-block-theme-toggle-to-dark-button="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.to_dark_button) || '' %>" <% } %> >
在 themes/shiroi/layout/post.ejs
的末尾引入逻辑脚本:
1 2 3 4 <%- js('js/code-block-header.js') %> <%- js('js/code-block-copy.js') %> <%- js('js/code-block-theme-toggle.js') %> <%- js('js/code-block.js') %>
代码块昼夜切换主题样式,
在code_block:
中添加子选项
1 2 3 4 theme_toggle: enable: true to_light_button: icon/light-codeblock.svg to_dark_button: icon/dark-codeblock.svg
我们的设计是将点击代码块右上方的昼夜切换按钮,可以实现代码块内部的深浅主题切换。
当按钮是to_light_button
,切换至浅色主题。
当按钮是to_dark_button
,切换至深色主题。
代码块内部的主题由 light_theme dark_theme
配置,
它们可以是source/css
中的css
文件;
也可以是指向css
的 URL。
最终架构的基础布局的颜色由一组CSS变量控制,这些变量在 code-block-base.css
中定义并拥有浅色主题的默认值。
切换主题时,JavaScript仅在代码块的根元素(<figure>
)上添加一个 .theme-dark
类,这个类会激活一组新的CSS变量值,从而瞬间改变所有基础颜色。
vscode-modern-light.css
和 vscode-modern-dark.css
文件只负责定义对应主题下的语法高亮文本颜色 ,并且其规则分别被 .theme-light
和 .theme-dark
类所限定。
我们设计了一个 themes/shiroi/source/js/code-block.js
的统一脚本。所有DOM操作,包括创建标题栏、语言名称、主题切换按钮和复制按钮的逻辑,全部被整合到该脚本中,确保了正确的执行顺序,避免了时序冲突和元素重复创建。
类名切换能即时生效
我们在 layout/_partial/head.ejs
中同时加载 浅色和深色两个主题的CSS文件。
1 2 3 4 5 6 7 8 9 10 <!-- themes/shiroi/layout/_partial/head.ejs --> <% if (is_post()){ %> <% if (theme.code_block && theme.code_block.theme_toggle && theme.code_block.theme_toggle.enable) { %> <%- css(theme.code_block.theme_toggle.light_theme) %> <%- css(theme.code_block.theme_toggle.dark_theme) %> <% } else { %> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.css"> <% } %> <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script> <% } %>
设计核心样式
code-block-base.css
文件被重构为使用CSS变量。它定义了浅色主题的默认颜色,并包含一个 .theme-dark
类用于覆盖这些变量以实现深色主题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 figure .highlight { --code-bg : #f7f7f7 ; --header-bg : #f7f7f7 ; --table-bg : white; --gutter-bg : #f7f7f7 ; --border-color : #eee ; --lang-name-color : #555 ; background-color : var (--code-bg); } figure .highlight .theme-dark { --code-bg : #282c34 ; --header-bg : #21252b ; --table-bg : #282c34 ; --gutter-bg : #21252b ; --border-color : #3a4049 ; --lang-name-color : #abb2bf ; }
vscode-modern-dark.css
和 vscode-modern-light.css
这两个文件现在只包含被 .theme-dark
或 .theme-light
限定的文本颜色规则。这两个文件可以充分的自定义。
统一脚本 (code-block.js
)
这是所有魔法发生的地方。它在页面加载后执行以下操作:
遍历所有代码块。
检查并防止重复创建标题栏。
为每个代码块默认添加 .theme-light
类。
从 body
标签的 data-
属性中读取配置。
动态创建完整的标题栏,包括语言名称、主题切换器和复制按钮,所有相关函数均在此文件中定义。
为按钮绑定事件监听器。点击时,脚本只做一件事:在 <figure>
元素上切换 .theme-light
和 .theme-dark
类 。
至此,整个功能改造完成,系统稳定、高效且易于维护。在完成上述重构后,语法高亮功能得以完美实现。
注意1:解决根源上的类名不匹配问题
之前遇到的问题根源在于Hexo的 _config.yml
文件中的 highlight.hljs: false
配置,它阻止了hljs-
前缀的生成。我们通过以下方式解决了这个问题:
恢复配置: 我们将 _config.yml
中的 hljs
选项保持为 false
,确保代码块能正确地渲染为多行。
改造主题CSS: 我们对 vscode-modern-light.css
和 vscode-modern-dark.css
其中所有的 .hljs-
前缀全部移除,并修正了基础文本颜色的选择器,使其与渲染器输出的HTML完全匹配。
例如,在 vscode-modern-dark.css
中:
1 2 3 4 5 6 7 8 9 .post-content figure .highlight .theme-dark .code .hljs-keyword { color : #c678dd ; } .post-content figure .highlight .theme-dark .code .keyword { color : #c678dd ; }
通过这次修改,CSS规则最终与渲染器输出的HTML完全匹配,语法高亮功能也得以完美实现。
注意2: 解决样式冲突!我们采用高优先级地办法保证了功能的独立性和可维护性。
在调试过程中,我们发现全局样式对代码块造成了严重干扰。为了确保样式稳定生效,我们采取了以下方案:
在 themes/shiroi/source/css/post.css
中注释掉了旧的表格样式。
在 themes/shiroi/source/css/code-block-base.css
中使用了高优先级、高特异性 的CSS选择器来覆盖 其他冲突样式。
代码块标题栏 Mac 主题风格增强
代码块标题栏 Mac主题增强可以通过开关打开,在配置文件中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 code_block: style: default copy_button: icon/copy.svg theme_toggle: enable: true to_light_button: icon/light-codeblock.svg to_dark_button: icon/dark-codeblock.svg light_theme: css/vscode-modern-light.css dark_theme: css/vscode-modern-dark.css mac_enhancier: enable: true init_folded_status: true
此功能将在代码块标题栏左侧添加Mac风格的交通灯图标,点击交通灯可以实现代码块的折叠与展开。
当 mac_enhancier
启用且 init_folded_status
为 true
时,代码块将默认以折叠状态显示。
实现细节
CSS 文件 (themes/shiroi/source/css/code-block-mac-enhancer.css
) :
新建 themes/shiroi/source/css/code-block-mac-enhancer.css
,用于定义Mac风格标题栏的样式和代码块折叠/展开的动画效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 .code-block-header .mac-style { position : relative; padding-left : 70px ; } .code-block-header .mac-style .code-lang-name { text-align : center; flex-grow : 1 ; } .mac-traffic-lights { position : absolute; left : 15px ; top : 50% ; transform : translateY (-50% ); display : flex; gap : 6px ; } .mac-traffic-lights .dot { width : 12px ; height : 12px ; border-radius : 50% ; border : 0.5px solid rgba (0 , 0 , 0 , 0.1 ); } .mac-traffic-lights .dot .red { background-color : #ff5f56 ; } .mac-traffic-lights .dot .yellow { background-color : #ffbd2e ; } .mac-traffic-lights .dot .green { background-color : #27c93f ; } figure .highlight .is-collapsed table .hljs { max-height : 0 ; overflow : hidden; transition : max-height 0.3s ease-out; } figure .highlight table .hljs { max-height : 9999px ; transition : max-height 0.3s ease-in; }
JavaScript 逻辑 (themes/shiroi/source/js/code-block.js
) :
在 themes/shiroi/source/js/code-block.js
文件中,新增 macEnhancerEnable
和 initFoldedStatus
的配置读取,并在 macEnhancerEnable
启用时,根据 initFoldedStatus
的值来初始折叠代码块。
同时,为交通灯容器添加点击事件监听器,用于切换代码块的 is-collapsed
类。
以下是相关核心逻辑的更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 document .addEventListener ('DOMContentLoaded' , () => { const codeBlocks = document .querySelectorAll ('figure.highlight' ); const bodyDataset = document .body .dataset ; const copyButtonConfig = bodyDataset.codeBlockCopyButton ; const themeToggleEnable = bodyDataset.codeBlockThemeToggleEnable === 'true' ; const macEnhancerEnable = bodyDataset.codeBlockMacEnhancer === 'true' ; const initFoldedStatus = bodyDataset.codeBlockMacEnhancerInitFoldedStatus === 'true' ; const themeToggleConfig = { enable : themeToggleEnable, to_light_button : bodyDataset.codeBlockThemeToggleToLightButton , to_dark_button : bodyDataset.codeBlockThemeToggleToDarkButton , }; codeBlocks.forEach (codeBlock => { const addMacTrafficLights = (header ) => { header.classList .add ('mac-style' ); const trafficLightsContainer = document .createElement ('div' ); trafficLightsContainer.className = 'mac-traffic-lights' ; const redDot = document .createElement ('span' ); redDot.className = 'dot red' ; trafficLightsContainer.appendChild (redDot); const yellowDot = document .createElement ('span' ); yellowDot.className = 'dot yellow' ; trafficLightsContainer.appendChild (yellowDot); const greenDot = document .createElement ('span' ); greenDot.className = 'dot green' ; trafficLightsContainer.appendChild (greenDot); header.prepend (trafficLightsContainer); trafficLightsContainer.addEventListener ('click' , () => { codeBlock.classList .toggle ('is-collapsed' ); }); }; if (macEnhancerEnable) { addMacTrafficLights (header); if (initFoldedStatus) { codeBlock.classList .add ('is-collapsed' ); } } }); });
布局文件更新 :
themes/shiroi/layout/layout.ejs
:
在 <body>
标签中添加 data-code-block-mac-enhancer
和 data-code-block-mac-enhancer-init-folded-status
属性。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <body <% if (theme.image && theme.image.enable) { %> data-image-center="<%= theme.image.center ? 'true' : 'false' %>" data-image-caption="<%= theme.image.caption ? 'true' : 'false' %>" <% } %> <% if (theme.code_block) { %> data-code-block-copy-button="<%= theme.code_block.copy_button || '' %>" data-code-block-theme-toggle-enable="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.enable) ? 'true' : 'false' %>" data-code-block-theme-toggle-to-light-button="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.to_light_button) || '' %>" data-code-block-theme-toggle-to-dark-button="<%= (theme.code_block.theme_toggle && theme.code_block.theme_toggle.to_dark_button) || '' %>" <% if (theme.code_block.theme_toggle && theme.code_block.theme_toggle.mac_enhancier) { %> data-code-block-mac-enhancer="true" data-code-block-mac-enhancer-init-folded-status="<%= (theme.code_block.theme_toggle.mac_enhancier.init_folded_status) ? 'true' : 'false' %>" <% } else { %> data-code-block-mac-enhancer="false" data-code-block-mac-enhancer-init-folded-status="false" <% } %> <% } %> >
themes/shiroi/layout/_partial/head.ejs
:
条件性地加载 code-block-mac-enhancer.css
。1 2 3 4 <!-- Codeblock Mac Enhancer CSS --> <% if (theme.code_block && theme.code_block.theme_toggle && theme.code_block.theme_toggle.mac_enhancier && theme.code_block.theme_toggle.mac_enhancier.enable) { %> <%- css('css/code-block-mac-enhancer.css') %> <% } %>
最终调试和样式冲突 :
此功能的实现也遵循了之前解决类名不匹配和样式冲突的原则,确保了新样式能正确应用且不干扰其他部分。
(文章可选配置) 添加跟随目录
在主题配置文件中添加选项
1 2 3 toc: enable: true position: left
创建文件latout/_partial/toc.ejs
1 2 3 4 5 6 7 8 9 10 11 <% if (theme.toc && theme.toc.enable && page.toc !== false) { %> <aside class="toc-container toc-left"> <%- toc(post.content) %> </aside> <% } %> ``` 3. 在`layout/_partial/head.ejs`中注入配置 ```ejs <% if (theme.toc && theme.toc.enable) { %> <%- css('css/toc.css') %> <% } %>
配置样式,创建文件cource/css/_custom/toc.styl
添加标签页
执行hexo new page tags
, 在生成的index.md
的Front Matter
前添加
选项
1 2 3 4 title: tags date: 2025-06-26 13:52:55 type: "tags" layout: "tags"
创建文件layout/tags.ejs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <div class="tag-overview-container"> <!-- <h2 class="page-title">所有标签</h2> --> <div class="tag-card-grid"> <% site.tags.sort('name').each(function(tag){ %> <a class="tag-card" data-tag-name="<%= tag.name %>"> <div class="tag-card-icon"> <img src="<%= url_for('icon/tag-icon.svg') %>" alt="Tag Icon" style="width:1.2em;height:1.2em;vertical-align:middle;"> </div> <div class="tag-card-info"> <span class="tag-card-name"><%= tag.name %></span> <span class="tag-card-count">(<%= tag.posts.length %>)</span> </div> </a> <% }) %> </div> <div id="filtered-articles-container" class="filtered-articles-container"> <!-- Articles will be dynamically loaded here --> </div> <div id="all-posts-data" data-posts='<%- JSON.stringify(site.posts.map(function(post){ return {title: post.title, path: url_for(post.path), tags: post.tags.map(function(tag){ return tag.name; }), date: date(post.date, "YYYY-MM-DD")} })) %>'></div> </div>
创建脚本文件source/js/tags-filters.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 document .addEventListener ('DOMContentLoaded' , function ( ) { const allPostsDataElement = document .getElementById ('all-posts-data' ); const filteredArticlesContainer = document .getElementById ('filtered-articles-container' ); let allPosts = []; if (allPostsDataElement && allPostsDataElement.dataset .posts ) { try { allPosts = JSON .parse (allPostsDataElement.dataset .posts ); } catch (e) { console .error ('Error parsing posts data:' , e); } } const tagCards = document .querySelectorAll ('.tag-card' ); function renderArticles (articles ) { if (articles.length === 0 ) { filteredArticlesContainer.innerHTML = '<p>暂无相关文章。</p>' ; return ; } let html = '<ul class="filtered-article-list">' ; articles.forEach (article => { html += ` <li class="filtered-article-list-item"> <span class="filtered-article-date">${article.date} </span> <a href="${article.path} " class="filtered-article-title">${article.title} </a> </li> ` ; }); html += '</ul>' ; filteredArticlesContainer.innerHTML = html; } tagCards.forEach (card => { card.addEventListener ('click' , function (event ) { event.preventDefault (); const tagName = this .dataset .tagName ; const filteredPosts = allPosts.filter (post => { return post.tags && post.tags .includes (tagName); }); renderArticles (filteredPosts); }); }); });
创建样式文件tags.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 .tag-overview-container { max-width : 900px ; margin : 40px auto; padding : 20px ; background-color : #fff ; } .page-title { text-align : center; margin-bottom : 30px ; color : #333 ; font-size : 2em ; } .tag-card-grid { display : flex; flex-wrap : wrap; justify-content : center; } .tag-card { margin : 10px ; display : flex; align-items : center; padding : 15px ; border : 1px solid #eee ; border-radius : 10px ; text-decoration : none; color : inherit; box-shadow : 0 2px 5px rgba (0 ,0 ,0 ,0.05 ); transition : all 0.3s ease; } .tag-card :hover { transform : translateY (-3px ); box-shadow : 0 4px 10px rgba (0 ,0 ,0 ,0.1 ); } .tag-card-icon { width : 30px ; height : 30px ; margin-right : 10px ; display : flex; align-items : center; justify-content : center; } .tag-card-icon svg { width : 100% ; height : 100% ; fill : #007bff ; } .tag-card-info { flex-grow : 1 ; } .tag-card-name { font-size : 1.2em ; font-weight : bold; color : #333 ; } .tag-card-count { font-size : 0.9em ; color : #777 ; margin-left : 5px ; } .tag-item-group ,.tag-name ,.tag-name a ,.tag-name a :hover ,.tag-post-list ,.tag-post-list-item ,.tag-post-list-item a ,.tag-post-list-item a :hover ,.tag-post-list-item time { display : none; } .filtered-articles-container { margin-top : 40px ; padding-top : 20px ; border-top : 1px solid #eee ; } .filtered-article-list { list-style : none; padding-left : 0 ; } .filtered-article-list-item { padding : 10px 0 ; border-bottom : 1px dotted #f0f0f0 ; } .filtered-article-list-item :last-child { border-bottom : none; } .filtered-article-title { text-decoration : none; color : #333 ; font-size : 1.1em ; transition : color 0.2s ease; } .filtered-article-title :hover { color : #007bff ; }
添加about页(同步Github Profile)
本地克隆远端仓库git clone https://github.com/Kytolly/Kytolly.git
编写站点目录下编写同步脚本scripts/sync-about-page.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const fs = require ('fs' );const path = require ('path' );const LOCAL_README_PATH = path.join (__dirname, '..' , 'resume' , 'Kytolly' , 'README.md' );const ABOUT_PAGE_PATH = path.join (__dirname, '..' , 'source' , 'about' , 'index.md' );async function syncAboutPage ( ) { try { console .log ('Reading local README content...' ); let githubReadmeContent = fs.readFileSync (LOCAL_README_PATH , 'utf8' ); githubReadmeContent = githubReadmeContent.replace (/<\>.*?<\>/g , '' ); githubReadmeContent = githubReadmeContent.replace (/!\[.*\]\(https:\/\/github-readme-stats\.vercel\.app\/api.*?\)/g , '' ); githubReadmeContent = githubReadmeContent.replace (/<script src=".*?<\/script>/g , '' ); githubReadmeContent = githubReadmeContent.replace (/##\s*Repository files navigation[\s\S]*?##\s*About/g , '## About' ); githubReadmeContent = githubReadmeContent.split ('## About' )[0 ] + '## About\n' + githubReadmeContent.split ('## About' )[1 ]; githubReadmeContent = githubReadmeContent.replace (/\s*###\s*\n/g , '' ); const existingContent = fs.readFileSync (ABOUT_PAGE_PATH , 'utf8' ); const frontMatterEndIndex = existingContent.indexOf ('---' , 3 ); let frontMatter = '' ; if (frontMatterEndIndex !== -1 ) { frontMatter = existingContent.substring (0 , frontMatterEndIndex + 3 ); } else { frontMatter = `--- title: 关于 date: ${new Date ().toISOString().split('T' )[0 ]} 12:00:00 layout: page --- ` ; } const newContent = `${frontMatter} \n\n${githubReadmeContent} ` ; fs.writeFileSync (ABOUT_PAGE_PATH , newContent, 'utf8' ); console .log ('About page synchronized successfully with local README.' ); } catch (error) { console .error ('Error synchronizing about page:' , error.message ); } } syncAboutPage ();
创建页面hexo new page about
主题外配置
启用 syzoj_renderer
安装渲染器 hexo-renderer-syzoj-renderer
1 2 npm uninstall hexo-renderer-marked npm install hexo-renderer-syzoj-renderer
在配置文件_config_yml
添加选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 syzoj_renderer: cache_file: cache.json highlighter: hexo options: highlight: expandTab: null wrapper: - <pre><code> - </code></pre> markdownItMergeCells: false markdownIt: html: true breaks: false linkify: true typographer: false markdownItMath: inlineOpen: $ inlineClose: $ blockOpen: $$ blockClose: $$
启用 giscus
在站点配置文件_config.yml
添加选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 giscus: enable: true repo: Kytolly/MyGiscus repo_id: R_kgDOK2X_9Q category: Announcements category_id: DIC_kwDOK2X_9c4Cbicm mapping: pathname strict: 0 reactions_enabled: 1 emit_metadata: 1 input_position: top theme: preferred_color_scheme lang: zh-CN
创建文件layout/_partial/giscus.ejs
,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <% if (config.giscus .enable && (page.giscus !== false ) ) { %> <script src ="https://giscus.app/client.js" data-repo ="<%= config.giscus.repo %>" data-repo-id ="<%= config.giscus.repo_id %>" data-category ="<%= config.giscus.category %>" data-category-id ="<%= config.giscus.category_id %>" data-mapping ="<%= config.giscus.mapping %>" data-strict ="<%= config.giscus.strict %>" data-reactions-enabled ="<%= config.giscus.reactions_enabled %>" data-emit-metadata ="<%= config.giscus.emit_metadata %>" data-input-position ="<%= config.giscus.input_position %>" data-theme ="<%= config.giscus.theme %>" data-lang ="<%= config.giscus.lang %>" data-loading ="<%= config.giscus.loading %>" crossorigin ="anonymous" async > </script > <% } %>
在layout/post.ejs
文件末尾注入配置
1 2 <!-- 插入giscus评论区 --> <%- partial('_partial/giscus') %>
若有必要在某个文章中关闭评论区,在文章的Front Matter
添加