/
media.ts
139 lines (130 loc) · 4.23 KB
/
media.ts
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
import MarkdownIt from "markdown-it";
import Renderer from "markdown-it/lib/renderer.mjs";
import Token from "markdown-it/lib/token.mjs";
import { default as directive, DirectiveMap } from "../directive.js";
/**
* 从资源的链接参数(?vw=...&vh=...)里读取尺寸,生成防抖容器的 style 属性。
*
* @param url 资源的链接
* @return style 属性字符串
*/
function getSizeStyle(url: string) {
const urlParams = new URLSearchParams(url.split("?")[1]);
// parseFloat(null) 返回 NaN 也是可以的
const vw = parseFloat(urlParams.get("vw")!);
const vh = parseFloat(urlParams.get("vh")!);
if (!(vw && vh)) {
return "";
}
return `style='--width:${vw}px;--aspect:${vw}/${vh}'`;
}
/**
* 提取了属性部分的通用逻辑,首先是里移出了 src 避免顶层元素有多余的属性,
* 另外使用了 MarkdownIt 的 renderAttrs()。
*
* TODO: 这里修改了 Token,但应该不会有后续插件会去用它吧。
*/
function blockAttrs(renderer: Renderer, token: Token) {
const i = token.attrIndex("src");
const src = token.attrs![i][1];
token.attrs!.splice(i, 1);
token.attrJoin("class", "md-center");
return [src, renderer.renderAttrs(token).trimStart()];
}
/**
* 自定义图片的渲染,相比默认的来说多了标签元素、懒加载、以及居中。
*
* 【不用 figure 元素】
* <figure> 介绍里提到即使移除也不影响 main flow,但文章里的图可能跟上下文联系紧密,
* 似乎并不能代替 <img> + <span>。
*
* 【防止布局抖动】
* 通过 URL 里携带的宽高信息,设置图片的宽高比,防止布局抖动,下面的 GIF 视频同理。
*
* 【加载指示器】
* 图片本身就有在不完全加载的时的显示方式,比如从上往下显示或者渐进式图片。
* 如果无法加载,下面的标签也能表明空白区域是图片,所以没有必要用菊花图。
*/
function renderImage(this: MarkdownIt, tokens: Token[], i: number, _: any, __: any, self: any) {
const token = tokens[i];
token.attrs!.splice(token.attrIndex("alt"), 1);
const [src, wrapperAttrs] = blockAttrs(self, token);
const label = this.utils.escapeHtml(token.content);
// MarkdownIt 遵守 CommonMark 规范对单引号不转义,所以 alt 必须用双引号。
return $HTML`
<span ${wrapperAttrs}>
<a
${getSizeStyle(src)}
class='md-inspect'
href="${src}"
target='_blank'
rel='noopener,nofollow'
>
<img data-src="${src}" alt="${label}" class='md-img' crossorigin>
</a>
${label ? `<span class='md-alt'>${label}</span>` : ""}
</span>
`;
}
/**
* 各种自定义指令在本站页面的渲染实现,渲染出来的 HTML 必须配合下面的懒加载使用。
*
* 对于其它的环境,比如 RSS,使用的是另外的渲染方案。
*/
const mediaMap: DirectiveMap = {
// 大部分浏览器只允许无声视频自动播放,不过 GIF 视频本来就是无声的。
gif(token, md) {
const alt = md.utils.escapeHtml(token.content);
const [src, wrapperAttrs] = blockAttrs(this, token);
return $HTML`
<p ${wrapperAttrs}>
<video
${getSizeStyle(src)}
class='gif'
crossorigin
loop
muted
data-src='${src}'
/>
${alt ? `<span class='md-alt'>${alt}</span>` : ""}
</p>
`;
},
video(token, md) {
let poster = md.normalizeLink(token.content);
if (!md.validateLink(poster)) {
poster = "";
}
const [src, wrapperAttrs] = blockAttrs(this, token);
return $HTML`
<p ${wrapperAttrs}>
<video
class='md-video'
controls
crossorigin
poster="${poster}"
data-src="${src}"
/>
</p>
`;
},
audio(token, md) {
const alt = md.utils.escapeHtml(token.content);
const [src, wrapperAttrs] = blockAttrs(this, token);
return $HTML`
<p ${wrapperAttrs}>
<audio controls data-src="${src}" crossorigin/>
${alt ? `<span class='md-alt'>${alt}</span>` : ""}
</p>`;
},
};
/**
* 自定义媒体元素的前端版,覆盖 Media 插件和默认的图片渲染器,
* 在这里,媒体将被渲染成具有更复杂的布局的元素,同时还启用了延迟加载。
*
* 添加该插件会同时添加 Directive 插件。
*/
export default function (md: MarkdownIt) {
md.use(directive, mediaMap);
md.renderer.rules.image = renderImage.bind(md);
}