-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathindex.js
129 lines (119 loc) · 3.78 KB
/
index.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
const nunjucks = require('nunjucks')
const path = require('path')
const fs = require('hexo-fs')
const url_for = require('hexo-util').url_for.bind(hexo)
const { promisify } = require('util')
const TEMPLATE_PATH = path.resolve(__dirname, 'valkyr.njk')
const STYLE_PATH = path.resolve(__dirname, './style.css')
const REG_NAMED_ARG = new RegExp(/\[([^=]+)=(.+)\]/)
const fetch = require('cross-fetch');
// NOTE: v1.0.0 及以上的 cheerio 要求使用 node v18, 否则会找不到 ReadableStream
const cheerio = require('cheerio');
const { get } = require('lodash');
const DEFAULT_ICON_SELECTOR = 'head link[rel~=icon]';
const DEFAULT_ICON_ATTR = 'href';
const DEFAULT_DESC_SELECTOR = 'head meta[name~=description]';
const DEFAULT_DESC_ATTR = 'content';
/**
* [origin]: {
* request: Promise;
* result: string;
* }
*/
const metaRequestMap = {
}
// 每次处理到 valkyrurl 标签时,调用此函数
// 处理完后返回 html 字符串
hexo.extend.tag.register(`valkyrurl`, async function(args, content){
const opts = {}
args.forEach(arg => {
const matched = arg.match(REG_NAMED_ARG)
opts[matched[1]] = matched[2]
})
let title = opts.title || opts.url;
let avatar = opts.avatar;
let url = opts.url;
let desc = opts.desc;
const enableAutoImg = get(hexo.config, 'valkyr_url.auto_img');
const enableAutoDesc = get(hexo.config, 'valkyr_url.auto_desc');
if (enableAutoImg || enableAutoDesc) {
const { $: doc, origin } = await getMetaOfUrl(opts.url);
if (enableAutoImg) {
const autoImgUrl = await getImageUrlOfMeta(doc, origin);
if (autoImgUrl) {
avatar = autoImgUrl;
}
}
if (enableAutoDesc) {
const autoDesc = await getDescOfMeta(doc);
if (autoDesc) {
desc = autoDesc;
}
}
}
const data = {
title,
url,
desc,
// TODO: Support default image or failed image placeholder
avatar,
}
const render = promisify(nunjucks.renderString)
const tpl = fs.readFileSync(TEMPLATE_PATH)
return render(tpl, data)
}, {
async: true
})
async function getMetaOfUrl(url) {
let request;
let $;
let origin;
if (url) {
try {
const urlObj = new URL(url);
origin = urlObj.origin;
if (metaRequestMap[origin]) {
// 同 hostname 直接使用缓存
request = metaRequestMap[origin];
} else {
request = metaRequestMap[origin] = fetch(origin, { timeout: 5000 });
}
const resp = await request;
const respType = resp.headers.get('content-type');
// 非 html content 不处理
if (respType.includes('text/html')) {
const text = await resp.text();
$ = cheerio.load(text);
}
} catch (err) {
// console.log(`debug: getMetaOfUrl 失败 url=[${url}]`);
}
}
return { $, origin };
}
async function getImageUrlOfMeta($, origin) {
if (!$) {
return;
}
let result;
let siteIconUrl = $(DEFAULT_ICON_SELECTOR).attr(DEFAULT_ICON_ATTR);
const autoImgParsers = get(hexo.config, 'valkyr_url.auto_img_parsers') || [];
for (let conf of autoImgParsers) {
const { pattern, attr, selector } = conf;
if (!pattern || !attr || !selector) continue;
if (!(new RegExp(pattern).test(origin))) continue;
const url = $(selector).attr(attr);
if (url) {
siteIconUrl = url;
break;
}
}
result = (new URL(siteIconUrl, origin)).toString();
return result;
}
async function getDescOfMeta($) {
if (!$) {
return;
}
return $(DEFAULT_DESC_SELECTOR).attr(DEFAULT_DESC_ATTR);
}