HTML 与 Markdown 锚点

当使用 Markdown 撰写大篇幅或者目录分级精细的文章时, 锚点的作用就显得很重要, 它能够支持从一个页内位置跳转到另一个位置, 然后为浏览器生成一次浏览历史记录, 当点击 "后退" 时能够返回到锚点跳转前的位置.

博主非 HTML 专业选手, 如果有术语使用错误还请见谅(欢迎纠错).

而锚点的原理就是 HTML 的页内超链接, 使用到了 HTML 的 id 属性和 <a> 标签的 href 属性.

HTML id 属性 | 菜鸟教程: https://www.runoob.com/tags/att-global-id.html

HTML <a> href 属性 | 菜鸟教程: https://www.runoob.com/tags/att-a-href.html

简单的表示就是在一个 HTML 标签内定义了这个标签的 id 后, 在一个 <a> 标签中就可以将 href 属性定义为该标签中的 id 的值, 就能超链接到这个 id 的位置实现锚点的效果.

有意思的是, <a>标签的 "a" 就是 "Anchor"(锚) 的缩写.

HTML 基础 - 学习 Web 开发 | MDN: https://developer.mozilla.org/zh-CN/docs/Learn/Getting_started_with_the_web/HTML_basics#链接

可以打开这篇由菜鸟教程提供的 HTML 实例页面:

点击展开

菜鸟教程的演示页面

打开浏览器开发者工具查看页面元素:

打开开发者工具查看元素

在这个页面中标题 <h2> 的中的 <a> 标签的 id 被定义为 "top", 而在这个 HTML 页面最底端的 <a> 标签将 href 属性定义为 "#top"* 则表示对此超链接执行 "锚点" 的作用, 浏览中点击这个超链接将滚动到定义锚点的那个点上.

*#top: 在 HTML 5 中, 当href属性指定为这个值的时候是具有回到页面顶部的作用的.

<a> - HTML(超文本标记语言) | MDN: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a#attr-href

而关于为什么要使用 "#" 作为超链接属性中指定为页内超链接的 "锚点" 标识, 在 统一资源标识符的语法(URI) 中就已经有规定: 统一资源标识符的语法(URI) | Anchor

标识互联网上的内容 - HTTP | MDN: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web

如同向服务器发送查询请求使用 "?" 一样, "#" 定义为锚定到页内某一个点也是约定好的, 使用 "#" 及其后的的 "片段标识符" 不会向服务器发送请求, 在 HTML 文档中将会被指向页内存在定义的一个锚点.


在 Markdown 中使用

对于知道 MD 的人来说, 或多或少都了解一点 HTML 的知识.

在 MD 中使用锚点决定于渲染器如何对 MD 源文档的进行解析渲染, 将其如果渲染器将 MD 文档中的标题语法解析并渲染成 HTML 后的 <h1> / <h2> 等的 HTML 标签内不会自动添加 id 属性, 那么使用就无法在 MD 文档中使用锚点;
如同在一些在线 MD 转换 HTML 的工具一样, 解析器只会将 MD 标题转换为简单的 HTML 标题:

一个 Markdown 转 HTML 在线工具


而如果要支持锚点, 那么渲染器应该这样处理这段 MD 语句:

MD 语句:

1
# 标题

使渲染后支持锚点的 HTML 语句:

1
2
3
<h1 id="标题">
标题
</h1>

如果实现以上的渲染转换, 那么就能使用锚点, 如下的 MD 语句:

1
[跳转到标题](#标题)

渲染后的 HTML 语句:

1
2
3
4
5
<p>
<a href="#标题">
跳转到标题
</a>
</p>

可以看到渲染后的 HTML 语句 <a> 标签中 href 正好被定义成了上文中 <h1>id, 所以能借此实现锚点的功能.


转义与消歧义

另外渲染器如果会自动定义这些 MD 标题在 HTML 的标题中的 id 属性, 也必须要知道其对此转义消歧义*的方法.

*消歧义: 因为一个渲染后的 HTML 页面内会存在有相同标题的情况, 这些标题可能同级也可能不同级.

转义

例如, 我要在这篇文章中使用 MD 语法中的链接语法要将超链接设置为跳转到本小节的上一级标题 "在 Markdown 中使用", 那么应该这样写 MD 的链接语句:

1
[跳转到本节上级标题](#在-Markdown-中使用)

渲染后:

跳转到本节上级标题 (点击即可跳转)

如果你打开浏览器开发者工具查看上面这个超链接的 HTML 代码应当是这样的:

1
2
3
<a href="#%E5%9C%A8-Markdown-%E4%B8%AD%E4%BD%BF%E7%94%A8" data-pjax-state="anchor">
跳转到本节标题
</a>

去掉无关的 Pjax 属性 data-pjax-state 最后是这样的:

1
2
3
<a href="#%E5%9C%A8-Markdown-%E4%B8%AD%E4%BD%BF%E7%94%A8">
跳转到本节标题
</a>

其中所有的中文字符为了转义变成成了一串编码, 而这个编码则是常提到的 URL 编码(又称 "百分号编码"):

百分号编码 - 维基百科: https://zh.wikipedia.org/wiki/百分号编码

具体编码原理及规则可以参阅阮一峰老师的这篇文章:

关于URL编码 - 阮一峰的网络日志: https://www.ruanyifeng.com/blog/2010/02/url_encoding.html


当然大可不必为了写 MD 而去为每个不支持在 URL 中使用的字符进行编码, 因为浏览器会自动应对大多数的编码, 列如访问上文提到的维基百科中的 "百分号编码" 的词条, 虽然浏览器地址栏中显示在地址栏中地址末尾的中的依旧是 "百分号编码";

浏览器地址栏显示中文字符

但如果你点击地址栏全选然后复制粘贴到记事本就会是这样的:

https://zh.wikipedia.org/wiki/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81

这就是浏览器自动编码的例子, 所以为了在支持的 MD 渲染器中使用应该要要注意的是我们在写 MD 标题的时候会用到的特殊字符以及空格会被渲染器如何编码, 除此之外的中文字符反而不用担心, 因为浏览器会自动编码中文字符.

另外在上一个一级章节 #锚点与页内超链接 的末尾提到以 # 开头的 id 属性不会向服务器发送请求, 所以编码转义与消歧义也是只需要考虑当前浏览器中即可.

消歧义

在最开头注释已经提到过, 同一个 HTML 文档中会存在相同的标题内容, 如果不进行消歧义那么就无法得知应该跳转到哪一个标题, 同时撰写的时候也无从得知怎样才能跳转到指定的同名标题位置.

渲染器的转义与消歧义方法

至于如何得知你在用的, 支持自动定义标题 id 的 MD 渲染器是怎么处理特殊字符及空格的, 最好的办法就是实际写出一份包含特殊符号标题的 MD 文档让它去渲染出来, 然后再去查看渲染后的 HTML 内容,

比如像在我的 Markdown 语法测试中的一样:

Markdown 语法测试 | CXPLAY World: /works/markdown_test.html#特殊字符转义

特殊字符转义 | 浏览器

按照实际渲染出来的 HTML 元素中可以得出哪些字符不会被转义, 哪些字符会被转义, 甚至有些字符转义还要加特定数字编号.

依照方便书写的原则就可以很轻易整理出实际写作中使用标题锚点中如何写出对应标题中含有的特殊字符转义:

特殊字符转义 | HTML

稍加编辑:

特殊字符转义 | 结果

具体流程为: 删除无用元素和符号 - 回编码字符 - 对照页面查找问题字符 - 整理字符转义

原字符 转义后
< -lt-
> -gt-
空格 -
? -
- -
= -
[ -
] -
; -
-
\ -
, -
. -
/ -
+ -
| -
~ -
! -
@ -
# -
$ -
% -
^ -
& -
* -
( -
) -
_ -
+ -
{ -
} -
: -
查看纯文本
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
“           id= “
< id= -lt-
> id= -gt-

空格 id= -
? id= -
- id= -
= id= -
[ id= -
] id= -
; id= -
’ id= -
\ id= -
, id= -
. id= -
/ id= -
+ id= -
| id= -
~ id= -
! id= -
@ id= -
# id= -
$ id= -
% id= -
^ id= -
& id= -
* id= -
( id= -
) id= -
_ id= -
+ id= -
{ id= -
} id= -
: id= -

<!-- blog.cxplay.org -->

可以看出, 几乎大部分的特殊字符都被我正在使用的 Hexo 默认渲染器 hexo-renderer-marked 编码转义成了 - 并且由上面多个 id 即将被定义为 - 的时候, 这个渲染器进行消歧义的方法是:
为相同的 id 属性加上 -number 其中 number 代表按照这个相同的 id属性出现的顺序来命名的次数, 比如 id="标题"出现了第三次, 则命名为 id="标题-3", 以此类推, 第四次就是 id="标题-4".


结论

由上可以得出在以 hexo-renderer-marked 为渲染器的 Hexo 中, 要获得转义后的标题id只需要利用好-替换特殊字符及空格, 面对相同的标题要获取消歧义后的id则应该使用 -number的规律按顺序来命名.
此外参照 hexo-renderer-marked 的文档说明, 也可以得知 id 是可以配置开关还能自定义格式:

hexojs/hexo-renderer-marked: https://github.com/hexojs/hexo-renderer-marked#options


另外如果你嫌去试渲染器渲染方式的办法太过于繁琐, 也可以用下一节的 "自定义锚点".

自定义锚点

在上面了解过 Markdown 锚点的原理之后那么还可以利用自定义 HTML 标签 id 实现"自定义锚点", 能够跳转到文章内的任意位置不只局限于标题 <h1> 等标签.
具体思路如下:

  1. 在 Markdown 内需要被定位到这里的位置写入一个 <p> 标签并定义它的 id:

    1
    2
    3
    <p id="001">
    跳转到此
    </p>
  2. 在引导可进行点击跳转的位置写入一个 Markdown 链接, 链接内容就是被定义的 <p> 标签的 id:

    1
    [点击跳转](#001)

    或是直接写成 HTML 的 <a> 标签也可以:

    1
    2
    3
    <a href="#001">
    点击跳转
    </a>

当然你不写 <p> 的内容将它变成一个 "隐形锚点" 那也是可以的.
如上完成一个简单的页内单向锚点跳转, 点击超链接即可跳转到对应位置, 再点击浏览器后退即可返回上一个位置.

双向锚点

这所谓 "双项锚点" 其实是我乱起的名字, 它的具体作用就是让阅读者点击锚点到对应位置之后在那个位置放一个反向的锚点, 阅读者可以点击这个锚点放回到起始位置, 适用于不喜欢用浏览器后退功能的人.

使用方法如下:

  1. 在 Markdown 内需要被定位到位置写入一个 <p> 标签并定义它的 id; 再嵌套一个 <a> 标签并定义它的 id, 将 href 内容写为返回跳转地 <a> 标签的 id:

    1
    2
    3
    4
    5
    <p id="001">
    <a href="#002">
    点击返回原位置
    </a>
    </p>
  2. 在引导可进行点击跳转的位置写上一个 <a> 标签, href 内容是定义的 <p> 标签的 id; 再将这个 <a> 标签的 id 定义为上个嵌套在 <p> 标签内的 <a> 标签的 href 内容:

    1
    2
    3
    <a id="002" href="#001">
    点击跳转
    </a>

    要是看得眼花可以外套一个 <p> 标签分别定义 id 和进行跳转:

    1
    2
    3
    4
    5
    <p id="002">
    <a href="#001">
    点击跳转
    </a>
    </p>

这里 "双向锚点" 其实是完全利用了 Markdown 对 HTML 的兼容, 用纯 HTML 互相跳转各个标签的 id .(因为 Markdown 链接不能定义其转换后的 <a> 标签的 id.)

后记

根据 Markdown 中使用锚点的实现方法以及后面的 "双向锚点" 可以看出这种使用方法并不属于 Markdown 语法, 至少不属于原生语法, 是一种利用 Markdown 渲染到 HTML 过程中的特性再由 Markdown 渲染器决定的 Markdown 扩展语法. 如果要使用 Markdown 锚点就必须明白渲染器对标题 ID 的处理方法, 再去配合其方法利用 Markdown 链接实现锚点. 或者直接使用 HTML 语法进行锚点设置和跳转.