Hexo框架renderer源代码的部分注释

本文最后更新于 2025年5月12日 晚上

node_modules/hexo-renderer-marked/lib/renderer.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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// 在源代码中添加 `console.log()` ,执行 generate 命令时,查看控制台输出的测试数据。clean 不能漏!

'use strict';

const { marked } = require('marked');

let JSDOM,
createDOMPurify;

const { encodeURL, slugize, stripHTML, url_for, isExternalLink, escapeHTML: escape, unescapeHTML: unescape } = require('hexo-util');
const { basename, dirname, extname, join } = require('path').posix;
const rATag = /<a(?:\s+?|\s+?[^<>]+\s+?)?href=["'](?:#)([^<>"']+)["'][^<>]*>/i;
const rDlSyntax = /(?:^|\s)(\S.+)<br>:\s+(\S.+)/;

const anchorId = (str, transformOption) => {
return slugize(stripHTML(unescape(str)).trim(), { transform: transformOption });
};

function mangleEmail(text) {
let out = '';
let i,
ch;

const l = text.length;
for (i = 0; i < l; i++) {
ch = text.charCodeAt(i);
if (Math.random() > 0.5) {
ch = 'x' + ch.toString(16);
}
out += '&#' + ch + ';';
}

return out;
}

const renderer = {
// Add id attribute to headings
heading({ tokens, depth: level }) {
let text = this.parser.parseInline(tokens);
const { anchorAlias, headerIds, modifyAnchors, _headingId } = this.options;

if (!headerIds) {
return `<h${level}>${text}</h${level}>`;
}

const transformOption = modifyAnchors;
let id = anchorId(text, transformOption);
const headingId = _headingId;

const anchorAliasOpt = anchorAlias && text.startsWith('<a href="#');
if (anchorAliasOpt) {
const customAnchor = text.match(rATag)[1];
id = anchorId(customAnchor, transformOption);
}

// Add a number after id if repeated
if (headingId[id]) {
id += `-${headingId[id]++}`;
} else {
headingId[id] = 1;
}

if (anchorAliasOpt) {
text = text.replace(rATag, (str, alias) => {
return str.replace(alias, id);
});
}

// add headerlink
return `<h${level} id="${id}"><a href="#${id}" class="headerlink" title="${stripHTML(text)}"></a>${text}</h${level}>`;
},

link({ tokens, href, title }) {
const text = this.parser.parseInline(tokens);
const { external_link, sanitizeUrl, hexo, mangle } = this.options;
const { url: urlCfg } = hexo.config;

if (sanitizeUrl) {
if (href.startsWith('javascript:') || href.startsWith('vbscript:') || href.startsWith('data:')) {
href = '';
}
}
if (mangle) {
if (href.startsWith('mailto:')) {
const email = href.substring(7);
const mangledEmail = mangleEmail(email);

href = `mailto:${mangledEmail}`;
}
}

let out = '<a href="';

try {
out += encodeURL(href);
} catch (e) {
out += href;
}

out += '"';

if (title) {
out += ` title="${escape(title)}"`;
}
if (external_link) {
const target = ' target="_blank"';
const noopener = ' rel="noopener"';
const nofollowTag = ' rel="noopener external nofollow noreferrer"';
if (isExternalLink(href, urlCfg, external_link.exclude)) {
if (external_link.enable && external_link.nofollow) {
out += target + nofollowTag;
} else if (external_link.enable) {
out += target + noopener;
} else if (external_link.nofollow) {
out += nofollowTag;
}
}
}

out += `>${text}</a>`;
return out;
},

// Support Basic Description Lists
paragraph({ tokens }) {
const text = this.parser.parseInline(tokens);
const { descriptionLists = true } = this.options;

if (descriptionLists && text.includes('<br>:')) {
if (rDlSyntax.test(text)) {
return text.replace(rDlSyntax, '<dl><dt>$1</dt><dd>$2</dd></dl>');
}
}

return `<p>${text}</p>\n`;
},

// Prepend root to image path
image({ href, title, text }) {
// href 是引用路径。比如,md 文档中引用本地图片的 md 语法:![](test/1.jpg),"test/1.jpg" 就是 href。
//console.log("href 0: " + href );

const { options } = this;
const { hexo } = options;
const { relative_link } = hexo.config;
const { lazyload, figcaption, prependRoot, postPath } = options;

// 如果 href 不是外部资源(非#锚点、非协议相对路径、非HTTP(S)绝对路径)、relative_link 未启用、prependRoot 选项启用,执行代码块。
/*
锚点定位:#about;协议相对路径://cdn.example.com/logo.png;绝对URL:"http://" 或 "https://" 开头的路径。
relative_link 默认 false;prependRoot 默认 true。
*/
if (!/^(#|\/\/|http(s)?:)/.test(href) && !relative_link && prependRoot) {
// 符合条件的例子:"test/1.jpg" 和 "/test/2.jpg"

// 如果 href 是相对路径格式的本地资源,且 postPath 为真值,执行代码块。
/*
postPath 参考 module.exports = function(data, options) {...} 部分
postPath 默认声明赋值为 '',是假值。
当 marked.postAsset 选项启用时,postPath 为真值,例如 "source/_posts/test"。
*/
if (!href.startsWith('/') && !href.startsWith('\\') && postPath) {
// 符合条件的例子:"test/1.jpg"

// 获取文章资源文件夹的模型。
const PostAsset = hexo.model('PostAsset');
// findById requires forward slash
// 通过【拼接的路径】定位资源。
/*
postPath 与【常规的 md 语法引用的相对路径】拼接而得到的路径是错误的。
例如:postPath 为 source/_posts/test,
常规的相对路径为 test/1.jpg,
source/_posts/test.md (语法格式:![](test/1.jpg))
source/_posts/test/1.jpg
两者拼接得到的路径为 source/_posts/test/test/1.jpg,通过该路径是获取不到 1.jpg 资源的。
换句话说,下面的 href = asset.path 并不会被执行,
href 不会是资源的真实路径(posts/test/1.jpg),而仅是原值(test/1.jpg)。
*/
/*
而 postPath 与无法显示图片的相对路径拼接得到的路径才能获取到资源。
例如:postPath 为 source/_posts/test,
非常规的相对路径为 1.jpg,
source/_posts/test.md (语法格式:![](1.jpg),文档无法显示图片)
source/_posts/test/1.jpg
两者拼接得到的路径为 source/_posts/test/1.jpg,通过该路径能获取到 1.jpg 资源。
然后,经过下面的 href = asset.path,href 重新赋值为资源的真实路径。
这是 hexo 官方教程给出的处理方式!!!相当糟糕!!!
*/
// console.log("postPath/href: " + join(postPath, href.replace(/\\/g, '/')));
const asset = PostAsset.findById(join(postPath, href.replace(/\\/g, '/')));

// if (asset) console.log("---asset---" + asset);

// asset.path is backward slash in Windows
/*
如果资源存在,将 href 重新赋值为 asset.path。
asset.path 是资源的真实全路径,例如 posts/test/1.jpg,目录部分是 【permalink结构】。
*/
if (asset) href = asset.path.replace(/\\/g, '/');
}

//console.log("href 1:" + href );

/*
核心路径处理函数。
例如:当 marked.prependRoot = true 时(默认值为 true),prepend root 值到相对路径内部。
*/
//console.log("url_for.call(hexo, href): " + href );
href = url_for.call(hexo, href);
}
let out = `<img src="${encodeURL(href)}"`;
if (text) out += ` alt="${escape(text)}"`;
if (title) out += ` title="${escape(title)}"`;
if (lazyload) out += ' loading="lazy"';

out += '>';
if (figcaption && text) {
return `<figure>${out}<figcaption aria-hidden="true">${text}</figcaption></figure>`;
}
return out;
}
};

// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Lexer.js#L8-L24
const smartypants = (str, quotes) => {
const [openDbl, closeDbl, openSgl, closeSgl] = typeof quotes === 'string' && quotes.length === 4
? quotes
: ['\u201c', '\u201d', '\u2018', '\u2019'];

return str
// em-dashes
.replace(/---/g, '\u2014')
// en-dashes
.replace(/--/g, '\u2013')
// opening singles
.replace(/(^|[-\u2014/([{"\s])'/g, '$1' + openSgl)
// closing singles & apostrophes
.replace(/'/g, closeSgl)
// opening doubles
.replace(/(^|[-\u2014/([{\u2018\s])"/g, '$1' + openDbl)
// closing doubles
.replace(/"/g, closeDbl)
// ellipses
.replace(/\.{3}/g, '\u2026');
};

const tokenizer = {
// Support autolink option
url(src) {
const { autolink } = this.options;

if (!autolink) return;
// return false to use original url tokenizer
return false;
},

// Override smartypants
inlineText(src) {
const { options, rules } = this;
const { quotes, smartypants: isSmarty } = options;

// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Tokenizer.js#L643-L658
const cap = rules.inline.text.exec(src);
if (cap) {
let text;
if (this.lexer.state.inRawBlock || this.rules.inline.url.exec(src)) {
text = cap[0];
} else {
text = escape(isSmarty ? smartypants(cap[0], quotes) : cap[0]);
}
return {
type: 'text',
raw: cap[0],
text
};
}
}
};

module.exports = function(data, options) {
const { post_asset_folder, marked: markedCfg, source_dir } = this.config;
const { prependRoot, postAsset, dompurify } = markedCfg;
const { path, text } = data;

marked.defaults.extensions = null;
marked.defaults.tokenizer = null;
marked.defaults.renderer = null;
marked.defaults.hooks = null;
marked.defaults.walkTokens = null;

// exec filter to extend marked
this.execFilterSync('marked:use', marked.use, { context: this });

// exec filter to extend renderer
this.execFilterSync('marked:renderer', renderer, { context: this });

// exec filter to extend tokenizer
this.execFilterSync('marked:tokenizer', tokenizer, { context: this });

const extensions = [];
this.execFilterSync('marked:extensions', extensions, { context: this });
marked.use({ extensions });

let postPath = '';
// 如果文章(md文档)存在、启用文章资源文件夹、prependRoot选项启用、postAsset选项启用,执行代码块。
/*
post_asset_folder 默认 false,设置 true 开启。
marked.prependRoot 默认为 true。marked.postAsset 默认为 false。
path是特定文章在磁盘绝对路径,例如 "E:\ningc5-blog\hexo\source\_posts\test.md"。
*/
if (path && post_asset_folder && prependRoot && postAsset) {
// 获取文章的模型,包含元数据、所有文章的部分数据和其他信息。
const Post = this.model('Post');
// Windows compatibility, Post.findOne() requires forward slash
// 获取特定文章的全路径(相对于资源文件夹)。
/*
this.source_dir.length是资源文件夹在磁盘绝对路径的长度,例如 "E:\ningc5-blog\hexo\source\",长度为 27。
path.substring(this.source_dir.length),截取文章相对于资源文件夹的路径并替换\\为/,例如 "_posts/test.md"。
*/
const source = path.substring(this.source_dir.length).replace(/\\/g, '/');

// 通过该路径获取特定文章数据。
/*
文章数据中有该文章的输出目录(post.path),与permalink配置项的值结构有关。
例如:
当 "permalink=posts/:name/" 时,"path":"posts/test/";输出到该目录下的 index.html。
当 "permalink=posts/:name.html" 时,"path":"posts/test.html";输出到 test.html。
*/
const post = Post.findOne({ source });

// if (post) console.log("if post exist, post: " + post);

if (post) {
// 从 post 对象中提取 source 属性,并将其重命名为 postSource。post.source,例如 "_posts/test.md"。
const { source: postSource } = post;
// 将 source_dir(资源目录)、postSource 的目录部分、以及 postSource 的文件名(不带扩展名)拼接成一个完整路径。
/*
source_dir 配置项,默认值为 source;
dirname(postSource) 获取 postSource 的目录部分,例如 "_posts";
extname(postSource) 获取 postSource 的扩展名,例如 ".md";
basename(postSource, extname(postSource)) 获取 postSource 的文件名并移除扩展名,例如 "test"。
路径拼接结果:"source/_posts/test"。
*/
postPath = join(source_dir, dirname(postSource), basename(postSource, extname(postSource)));
}
}
//console.log("postPath: " + postPath );

let sanitizer = function(html) { return html; };

if (dompurify) {
if (createDOMPurify === undefined && JSDOM === undefined) {
createDOMPurify = require('dompurify');
JSDOM = require('jsdom').JSDOM;
}
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
let param = {};
if (dompurify !== true) {
param = dompurify;
}
sanitizer = function(html) { return DOMPurify.sanitize(html, param); };
}

marked.use({
renderer,
tokenizer
});
return sanitizer(marked.parse(text, Object.assign({
// headerIds was removed in marked v8.0.0, but we still need it
headerIds: true
}, markedCfg, options, { postPath, hexo: this, _headingId: {} })));
};

Hexo框架renderer源代码的部分注释
https://cornst.com/posts/Hexo框架renderer源代码的部分注释.html
作者
shaton沙桐
发布于
2025年4月24日
更新于
2025年5月12日
许可协议