Quartz Transfrom Pipleine
Quartz Transfrom Pipleine

Quartz 支持 Obsidian Callouts,但是 Callouts 非常难用,于是我想为 Quartz 实现Nunjucks模板引擎,就像 Hexo 那样。

首先,要明确它的渲染管线。第一步处理Transformer中有一步骤textTransform,在解析 markdown 前进行文本预处理。模板引擎所干的事只是替换文本,所以要在 textTransform 这里注册插件,这时源 Markdown 文件还没有被转化为 Markdown AST,可以进行字符串到字符串的操作。

造轮子

Nunjucks 的自定义标签注册很麻烦,所以我稍微借用了下 Hexo 封装好的自定义标签管理,简单来说,就是保存了一个 Nunjuck Enviroment,并封装了一系列注册、删除自定义标签的机制和相关类型,注册者只需关注标签的具体逻辑,而不是解析过程。

我删除了一些用不到的代码,但里面还有好多问题,碍于时间不去进一步精简了。

quartz/plugins/tags.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
import { Environment } from 'nunjucks';
import stripIndent from 'strip-indent';

interface TagFunction {
(args: any[], content: string): string;
}

class NunjucksTag {
public tags: string[];
public fn: TagFunction;

constructor(name: string, fn: TagFunction) {
this.tags = [name];
this.fn = fn;
}

parse(parser: any, nodes: any, lexer: any) {
const node = this._parseArgs(parser, nodes, lexer);
return new nodes.CallExtension(this, 'run', node, []);
}

_parseArgs(parser: any, nodes: any, lexer: any) {
const tag = parser.nextToken();
const node = new nodes.NodeList(tag.lineno, tag.colno);
const argarray = new nodes.Array(tag.lineno, tag.colno);

let token;
let argitem = '';

while ((token = parser.nextToken(true))) {
if (token.type === lexer.TOKEN_WHITESPACE || token.type === lexer.TOKEN_BLOCK_END) {
if (argitem !== '') {
const argnode = new nodes.Literal(tag.lineno, tag.colno, argitem.trim());
argarray.addChild(argnode);
argitem = '';
}

if (token.type === lexer.TOKEN_BLOCK_END) {
break;
}
} else {
argitem += token.value;
}
}

node.addChild(argarray);
return node;
}

run(context: any, args: any, body: any, callback: any) {
return this._run(context, args, '');
}

_run(context: any, args: any, body: any): any {
return Reflect.apply(this.fn, context.ctx, [args, body]);
}
}

const trimBody = (body: () => any) => {
return stripIndent(body()).replace(/^\n?|\n?$/g, '');
};

class NunjucksBlock extends NunjucksTag {
parse(parser: any, nodes: any, lexer: any) {
const node = this._parseArgs(parser, nodes, lexer);
const body = this._parseBody(parser, nodes, lexer);

return new nodes.CallExtension(this, 'run', node, [body]);
}

_parseBody(parser: any, nodes: any, lexer: any) {
const body = parser.parseUntilBlocks(`end${this.tags[0]}`);

parser.advanceAfterBlockEnd();
return body;
}

run(context: any, args: any, body: any, callback: any) {
return this._run(context, args, trimBody(body));
}
}

class Tag {
public env: Environment;

constructor() {
this.env = new Environment(null, {
autoescape: false
});
}

register(name: string, fn: TagFunction, ends: boolean = false): void {
if (!name) throw new TypeError('name is required');
if (typeof fn !== 'function') throw new TypeError('fn must be a function');

let tag: NunjucksTag;

if (ends) {
tag = new NunjucksBlock(name, fn);
} else {
tag = new NunjucksTag(name, fn);
}

this.env.addExtension(name, tag);
}

unregister(name: string): void {
if (!name) throw new TypeError('name is required');

const { env } = this;

if (env.hasExtension(name)) env.removeExtension(name);
}
}

export default Tag;

插件

def
1
2
3
4
5
6
7
export type QuartzTransformerPluginInstance = {
name: string
textTransform?: (ctx: BuildCtx, src: string | Buffer) => string | Buffer
markdownPlugins?: (ctx: BuildCtx) => PluggableList
htmlPlugins?: (ctx: BuildCtx) => PluggableList
externalResources?: (ctx: BuildCtx) => Partial<StaticResources>
}

这是插件的定义,创建源文件 quartz/plugins/transformers/nunjucks.ts,按照官方文档中的写法,搭建插件基本结构。在这个案例中,我们只需用到textTransform

quartz/plugins/transformers/nunjucks.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
import Tag from "../tags";
import { QuartzTransformerPlugin } from "../types"
import nunjucks, { Environment, Extension } from "nunjucks";

export interface Options { }

const tags = new Tag();

// 注册你想要的标签
tags.register("helloworld", () => 'helloworld parsed.')
tags.register("blocked", (args, con) =>
`parsed blocked, args [${args[0]}]: \n${con}`, true)
tags.register('hide', () => '', true);


const env = tags.env;

export const Nunjucks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
//const opts = { ...userOpts }
return {
name: "Nunjucks",
textTransform(ctx, src) {
return nunjucks
.compile(src.toString(), env)
.render();
},
}
}

最后,在 quartz.config.ts 中加入插件的引用即可。

总结

  • 编译和预处理时间增长,这也很无奈。
  • 没有处理 Nunjucks 模板引擎的异常,若模板错误,将中止 Build 过程。
  • 可以不使用该方法,而是使用CSS选择器自定义样式
    https://publish.obsidian.md/slrvb-docs/ITS+Theme/Image+Adjustments
    不过,要在 Quartz 中正常显示,需要把选择器中的 [alt] 改为 [width],因为它不支持图片说明文字,而会把 [XXXX]() 直接变为属性 width=XXXX。

萌ICP备20229066 | Build by C2iCs | Powered by Hexo and Stellar 1.27.0
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本“页面”访问 次 | 👀总访问 次 | 🍖总访客

开往-友链接力