近期给博客实现的有趣功能

近一个月的博客施工状况,一些好看有趣的新功能。

Favicon

博客有自己的图标了,一只 16x16 像素小沙耶,favicon 里用的是带红色背景版本。

saya_16x16_nobackground.png
saya_16x16_nobackground.png
saya_16x16_nobackground.png
saya_16x16_nobackground.png
saya_16x16_nobackground.png
saya_16x16.png
saya_16x16.png
saya_16x16.png
saya_16x16.png
saya_16x16.png

图片跟随博客内容以 CC BY-NC 4.0 协议发布,您可以在这里下载到它(PNG 格式)。

/ai /micros /now

添加了许多 Slash Pages

  • /ai:AI 使用说明。我认为有必要从关于页面单独提取成一个说明页;
  • /micros:我称它为「碎片」,包含简短的想法、公告、备忘录、一些我的悄悄话,当然您也可以理解为“微博”——微型博客。灵感来自于 robinwobin.dev/notes/
  • /now:我最近正在做什么。在看什么书、听什么音乐、写什么代码,或是在玩什么游戏。

404 页面

我还为博客添加了一个迟来的 Lain 主题 404 页面,传送门

里面还包含一条彩蛋音频,模仿 Lain 标题播报,虽然很可能被您的浏览器拦截播放。

Lain 里的音频来自早期 Mac 的语音合成程序 PlainTalk,我在这里找到了相同的音色 Whisper 制作它。

也推荐您去看看 fauux.neocities.org

Row

Row 只是普通地在行内并排展示内容,就像 Favicon 里并排展示图片那样。

将内容放在 #+begin_row 里:

#+begin_row
一些文本

- 一些列表项
- 两些列表项
- 三些列表项
- 不要乱玩中文

#+caption: 一些代码
#+begin_src python
print("Hello, World!")
#+end_src

#+caption: 一些图片
#+attr_html: :style width: 250px;
file:../assets/stickers/shimejisim/shimejisim_2.webp
#+end_row

就可以得到这样的效果:

一些文本

  • 一些列表项
  • 两些列表项
  • 三些列表项
  • 不要乱玩中文
print("Hello, World!")
shimejisim_2.webp
图1  一些图片 《蘑菇拟态日常》贴纸包

ox-html 默认将 #+begin_NAME 导出为 <div class="NAME"> ,所以 publish.el 不需要做任何处理。

CSS 中将 <div class="row"> 定义为 Flexbox 布局,子元素横向排列,这样就能并排了:

.row {
    display: flex;
    gap: 0.75em;
    align-items: flex-end;
    margin-block: 1em;
    overflow-x: auto;
}

只是 justify-content: center 来居中的话,当内容超出容器时,左侧内容被截断且没法滚动查看完全,所以要用 safe center

.row {
    justify-content: safe center;
}

而且还得防止子元素被压缩,不然屏幕小的时候图里的小人就会脑袋尖尖的:

.row {
    &>* {
        flex-shrink: 0;
    }
}

最后,已有的规则是窄屏情况下图片宽度占满全屏,这里得取消它:

.row {
    & img {
        max-width: none !important;
    }
}

代码块行号和高亮行

使用方式:

#+attr_html: :hl-lines 4
#+begin_src C -n
#include <stdio.h>

int main() {
  printf("Hello, World!\n");
  return 0;
}
#+end_src

渲染效果:

1#include <stdio.h>
2
3int main() {
4  printf("Hello, World!\n");
5  return 0;
6}

行号

Org-mode 内置行号支持,要在 #+begin_src 中加 -n 参数开启。

ox-html 默认会在行号后加冒号(1:),可以通过 export filter 去掉:

(defun blog/html-remove-linenr-colon (output backend _)
  (when (org-export-derived-backend-p backend 'html)
    (replace-regexp-in-string
     "\\(<span class=\"linenr\"> *[0-9]+\\): "
     "\\1"
     output)))

用 CSS 将行号对齐、加右侧竖线分隔,并设 user-select: none 防止复制代码时带入行号:

.linenr {
    display: inline-block;
    padding-inline-end: 0.75em;
    margin-inline-end: 0.75em;
    color: var(--ctp-macchiato-overlay0);
    border-inline-end: 1px solid var(--ctp-macchiato-surface2);
    text-align: right;
    user-select: none;
}

高亮行

通过 #+attr_html: :hl-lines 指定要高亮的行,支持单行和范围:

#+attr_html: :hl-lines 1-3,9-17
#+begin_src C -n
...
#+end_src

实现分三步。首先,自定义 src-block translator 把 :hl-lines 的值写进容器 <div> 的属性里:

(defun blog/html-src-block (src-block contents info)
  (let* ((attrs (org-export-read-attribute :attr_html src-block))
         (hl    (plist-get attrs :hl-lines))
         (output (org-html-src-block src-block contents info)))
    (if hl
        (replace-regexp-in-string
         "<div class=\"org-src-container\">"
         (format "<div class=\"org-src-container\" hl-lines=\"%s\">" hl)
         output t t)
      output)))

然后,另一个 export filter 扫描整个导出结果,解析 hl-lines 属性,将匹配行号的 <span class="linenr"> 加上 hl class:

(defun blog/html-highlight-lines (output backend _)
  (when (org-export-derived-backend-p backend 'html)
    (with-temp-buffer
      (insert output)
      (goto-char (point-min))
      (while (re-search-forward "hl-lines=\"\\([^\"]+\\)\"" nil t)
        (let* ((hl-lines (blog/parse-line-spec (match-string 1)))
               (pre-end (save-excursion
                          (when (re-search-forward "</pre>" nil t)
                            (copy-marker (point))))))
          (when pre-end
            (while (re-search-forward
                    "<span class=\"linenr\">\\( *[0-9]+\\)" pre-end t)
              (when (member (string-to-number (match-string 1)) hl-lines)
                (replace-match "<span class=\"linenr hl\">\\1")))
            (set-marker pre-end nil))))
      (buffer-string))))

最后,将 .linenr.hl 渲染为主题色:

.linenr.hl {
    color: var(--ctp-macchiato-teal);
    border-inline-end-color: var(--ctp-macchiato-teal);
}

也还不错吧,和以前博客里的样式不太一样,以前类似荧光笔涂了一整行,而现在就只是行号数字变色了。

Tabs

如果您看过卡拉彼丘 hpatchz 的 -c 选项或更早的一些文章,可能您已经见过 Tabs 了。

实现思路来自 your hex editor should color-code bytes,文章中利用 Tabs 对比展示不同代码,很漂亮。除此之外,这也是一篇非常有意思的文章,如果您对十六进制编辑器和代码高亮感兴趣的话。

这个方案无 JavaScript,利用 <details> 标签的互斥特性,同一 name<details> 组内只能有一个处于展开状态。

用法是在 #+begin_tabs 包裹若干 #+begin_details 块:

#+begin_tabs
#+attr_html: :summary Tab A :open t
#+begin_details
C 语言
#+begin_src c
#include <stdio.h>

int main() {
  printf("Hello, World!\n");
  return 0;
}
#+end_src
#+end_details

#+attr_html: :summary Tab B
#+begin_details
Python 语言
#+begin_src python
print("Hello, World!")
#+end_src
#+end_details
#+end_tabs

渲染效果:

Tab A

C 语言

#include <stdio.h>

int main() {
  printf("Hello, World!\n");
  return 0;
}
Tab B

Python 语言

print("Hello, World!")

publish.el 在处理 <details> 时,检测父块是否为 tabs,若是则注入 name 属性将它们绑为同一互斥组:

(in-tabs  (and parent
               (eq (org-element-type parent) 'special-block)
               (string= (downcase (org-element-property :type parent)) "tabs")))
...
(when in-tabs (format "name=\"tabs-%d\""
                      (org-element-property :begin parent)))

样式方面参考了 main.css,用 Grid 实现布局,一行标签栏(auto)、一行内容区(1fr),初始时只有一列:

.tabs {
    display: grid;
    grid-template-rows: auto 1fr;
    grid-template-columns: minmax(0, 1fr);
}

通过 :has(details:nth-child(n)) 动态调整列数,有几个 <details> 就分几列(手动写了五列的规则,再多可能也用不上了):

.tabs {
    &:has(details:nth-child(2)) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    &:has(details:nth-child(3)) {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    &:has(details:nth-child(4)) {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    &:has(details:nth-child(5)) {
        grid-template-columns: repeat(5, minmax(0, 1fr));
    }
}

每个 <details> 通过 subgrid 继承容器的行列定义,并同时占满整个网格区域,形成叠层:

.tabs {
    >details {
        display: grid;
        grid-template-rows: subgrid;
        grid-template-columns: subgrid;
        grid-row: 1 / -1;
        grid-column: 1 / -1;
    }
}

<summary> 固定在第一行,按 nth-child 对号入座到对应列,z-index: 1 保证始终可点击:

.tabs {
    >details {
        &:nth-child(1)>summary {
            grid-column: 1;
        }
        /* ... */
        &:nth-child(5)>summary {
            grid-column: 5;
        }

        >summary {
            grid-row: 1;
            z-index: 1;
        }
    }
}

::details-content 始终占据第二行全宽,展开时用 z-index 浮到叠层最上方:

.tabs {
    >details {
        &::details-content {
            grid-row: 2;
            grid-column: 1 / -1;
        }

        &::details-content {
            z-index: 1;
        }
    }
}

这样就实现了标签页的切换展开显示。

还有一些针对 Tabs 中只有代码块元素时的特殊规则,可以在 style.css 里看到。

貌似用到的一些 CSS 特性太“现代”,我也不确定大家的浏览器能不能支持。

Spoiler Tags

设计使用 [[spoiler:][文字]] 链接语法,这样就能在行内文本中插入了。

点击显示剧透:

我不会告诉您再次点击别处可以重新遮盖

publish.el 里注册自定义链接类型,导出为带 tabindex<span class="spoiler">

(org-link-set-parameters
 "spoiler"
 :follow #'ignore
 :export (lambda (_path desc backend _info)
           (when (org-export-derived-backend-p backend 'html)
             (format "<span class=\"spoiler\" tabindex=\"0\">%s</span>" desc))))

:follow #'ignore 防止在 Emacs 中点击时报错。tabindex="0"<span> 可聚焦,支持键盘 Tab 导航。

CSS 靠 :focus 切换显隐,默认文字透明,背景色遮盖,聚焦后还原:

.spoiler {
    color: transparent;
    background-color: var(--ctp-macchiato-surface2);
    cursor: pointer;
    user-select: none;
    transition: color 0.2s, background-color 0.2s;
}

.spoiler:focus {
    color: inherit;
    background-color: var(--ctp-macchiato-surface0);
    outline: none;
    user-select: text;
}

HTML5 fancy

publish.el 中为 ox-html 开启 HTML5 导出:

(setq org-html-doctype "html5"
      org-html-html5-fancy t)

开启后导出结构从 <div> + class 改为语义化 HTML5 标签。变化最明显的是图片,带有 #+caption 的图片原先导出为:

<div class="figure">
  <p><img src="..."></p>
  <p><span class="figure-number">Figure 1: </span>说明文字</p>
</div>

现在变成原生的 <figure><figcaption>

<figure>
  <img src="...">
  <figcaption><span class="figure-number">Figure 1: </span>说明文字</figcaption>
</figure>

文章标题区域也会被包进 <header>,正文内容则包进 <article>,各章节变成 <section>

相应地,CSS 选择器也需要跟进,不一一列出了。

还有呢

目前来看,已经可以赶上 MkDocs For Material 的效果了,甚至说——超过它。

还有许多有趣的东西正在实现……

Copyright © 2024 13m0n4de · CC BY-NC 4.0