Sy 源文件
什么是模板?模板,简单来说就是在一段文本中定义一些「变量」,这些变量在实际渲染的时候会按照一定的规则被替换为实际的值。
比如我们最常见的日记路径模板:
/daily note/{{now | date "2006/01"}}/{{now | date "2006-01-02"}}
在每天调用 Alt+5 快捷键创建日记的时候,思源就会渲染以上的模板,把其中由 {{}}
内部定义的变量替换为实际的值(在这里是日期),最后变为 /daily note/2024/01/01-31
这样具有实际意义的路径字符串。
思源的模板在功能上非常强大,然而实际用起来的感受——作为一个笔记软件的模板而言——老实说,使用起来还挺繁杂的。
这篇文章不会帮你完全掌握思源的模板功能,而是尽量提纲挈领地告诉大家思源模板功能的概况。如果你想要深入了解甚至熟练掌握——那你需要去认真看看相关文档,并抽空练习。
从总体上讲,我会把思源的模板功能划分为这么几个重要的版块:
-
基于 Golang 的模板语法
-
基本语法
{{ }}
vs.action{}
-
常用的模板函数
-
常用的流程控制
- 条件控制
- 循环控制
-
-
数据库中的用法
-
Markdown 块语法
{: }
块属性声明语法- 超级块排版声明语法
在正式讲解之前,有必要先把这几个版块之间的关系厘清楚:
-
Golang 模板语法和 Sprig
-
思源的模板的语法是基于 Golang 的模板引擎来实现的
-
Golang 的模板引擎并没有提供多少好用的模板函数,所以思源又内置了 Sprig 库——这个库提供了大量丰富的模板函数,来增强模板的功能性
-
基本语法
- 原始的 Golang 模板语法是
{{ xxx }}
- 但是在思源中
{{ }}
又是嵌入块的声明语法 - 为了避免冲突,开发者就自定义了
.action{ xxx }
语法来替换原始的{{ xxx }}
- 原始的 Golang 模板语法是
-
-
数据库模板列
- 思源目前的数据库当中,可以创建一个「模板列」,在这个列当中可以使用
.action{}
语法,这样就可以在模板列中根据对应行的内容动态展示其内容 - 一个类似的例子:就好比在 excel 中增加一列公式
- 思源目前的数据库当中,可以创建一个「模板列」,在这个列当中可以使用
-
Markdown 块语法
- Markdown 本身是没有块结构的;思源实现了一种基于 kramdown 的 markdown 方言;这个方言不属于 Golang 模板语法的一部分,而是属于思源自己的特殊语法
- 基于这种方言,思源就可以在模板文件中为块预定义自定义属性、通过特殊的语法创建复杂的超级块排版
- 注意:这个语法和 Golang 模板无关,在数据库 、路径模板的这些地方使用是不会起效果的!
前附:Test-Template 插件
在这篇教程之外,我上架了一个插件“测试模板语法”以方便大家测试 Golang 的模板语法。
你可以在阅读的过程中,使用这个插件对模板语法进行测试。插件的详细用法见说明文档。
思源 Golang 模板语法 Cheatsheet
基本的模板语法讲起来不够直观,这里我画了一个 cheatsheet 给大家展示,信息密度高一点会更加方便理解。
基础语法快速总结
-
使用
{{ }}
或者.action{}
来定义模板,里面一般是函数调用 -
函数的基本概念
- 函数可以接受零个或者若干个参数,进行计算后返回一个特定的值
- 函数的参数和返回值有特定的类型,不同类型之间不能混用
-
变量的赋值
- 如果使用
{{ now }}
这种语法,模板引擎会直接将其渲染为具体的值 - 但是如果使用
{{ $t := now}}
这种语法,那么now
的返回值会存储在变量$t
中而不会渲染内部具体的值,此后就可以使用$t
来引用这个变量
- 如果使用
-
使用函数
-
函数是一个封装好的功能包,输入一些参数,进行特定的计算,返回特定的值
-
传入函数的参数类型要和函数接受的参数类型匹配
-
|
语法:管道语法,这是一种在计算机领域非常常见的语法,相关参考
-
控制流语法快速介绍
Go 语言模板控制流的用法可以参考官方文档:https://pkg.go.dev/text/template#hdr-Actions
这里介绍最常用的:
-
if 语句:通过条件判断来有选择性地渲染、执行一部分模板语法
基本的用法是:如果
condition
中的条件被判定为 true,则渲染T1
否则渲染T2
{{if (condition)}} T1 {{else}} T0 {{end}}
例如:
{{ if eq (Weekday now) 1 }} 好好工作 {{ else }} 摸鱼! {{ end }}
在这里的 condition 中:
-
首先计算
Weekday now
获取今天的星期数 -
然后通过
eq
和 1 对比,看看是不是周一;- 如果是,就渲染「好好工作」
- 如果不是,就渲染「摸鱼!」
如果你不理解这里用到的
eq
函数,请不要焦虑,在后面的部分我们会详细说明在 if 语句中常常用到的函数。 -
-
range 语句
range 语句被用来在一个序列当中进行迭代,最常见的语法如下
{{ range $v := <slice> }} {{ $v }} {{ end }}
这里的含义是:
- 在一个可迭代的
<slice>
(通常是一个列表List
)中迭代 - 每次取出一个值赋值给
$v
- 我们就可以在 range 块内部访问这个
$v
变量
一个简单的案例如下:
{{ $list := list 1 2 3 4 }} {{ range $v := $list }} - Index: {{ $v }} {{ end }}
这里我们首先通过
list
函数创建一个列表(后面会介绍这个函数),然后在range
中迭代这个列表,依次访问每个值。渲染结果为:
- Index: 1
- Index: 2
- Index: 3
- Index: 4
- 在一个可迭代的
这里介绍的是最简单的控制流语法,还有更高级的用法请自行探索。
常用函数介绍
函数是想要用好模板功能必定绕不开的一座大山。在上面的 Cheatsheet 中,我们简要介绍过函数的基本概念——函数就是一个封装好的功能包,他可以接收零个或者多个(视函数而定)参数,并计算得到一个输出。
{{ now }} # now 函数,接收 0 个参数,输出当前的时间对象
{{ add 1 2 3 4}} # add 函数,接收任个 int (整数)参数,输出他们的累加结果
思源中支持的函数很多,有 Sprig 包支持的也有思源内置的。而实际在思源中常用的函数,我把他们分类为这几类:
-
算术计算
- 整数算术(int 类型)
- 浮点数算术(float 类型)
-
时间计算
-
字符串操作
-
列表计算
-
类型转换(偶尔)
-
逻辑运算(在写条件块的时候要用)
-
思源模板片段特殊函数
为了方便,我这里整理了最常用的一些函数,进行简单介绍。在介绍的过程中,遵循以下的格式:
-
Fun
代表一个没有参数的函数,比如now
-
Fun <int>, <int>
代表有几个固定数量参数的函数,比如sub <int> <int>
;<int>
代表了参数的类型,常用类型还有-
int
:整数类型int64
:64 位的整数类型,需要通过类型转换函数和int
类型做转换
-
float
:浮点数类型float64
:64 位的浮点数类型,需要通过类型转换函数和float
类型做转换
-
list
:列表类型 -
str
:字符串类型 -
bool
:布尔类型(true
或者false
) -
Time
:时间对象类型(这个类型会在后面有更加详细的解释)
-
-
Func [<int>,]
代表有不定长数量参数的函数,比如add
后面可以跟好多个整数
同时我还会给出一些使用范例,以及渲染后的效果,以供参考。
常用数值计算函数
-
Sprig 函数
-
整型 int 计算:完整文档见 https://masterminds.github.io/sprig/math.html
add [<int64>,]
:int64
,累加sub <int64> <int64>
:int64
,减法mul [<int64>,]
:int64
,累乘div <int64> <int64>
:int64
,整除mod <int64> <int64>
:int64
,求余min [<int64>,]
和max [<int64>,]
:int64
,求最小值和最大值
-
浮点型 float 计算:完整文档见 https://masterminds.github.io/sprig/mathf.html
-
注:Sprig 的数值运算是在 int64 和 float64 类型上进行的,但是很多函数只接受 int 或 float 类型,所以很多时候要配合类型转换函数来使用,这一点会在下面的小节中详细说明
-
-
思源内置数值函数
-
pow <int>
:指数计算,返回整数 -
powf <float>
:指数计算,返回浮点数 -
log <int>
:对数计算,返回整数 -
logf <float>
:对数计算,返回浮点数 -
FormatFloat <format str> <n float64>
:string
,说老实话我不能理解这个函数,这是一个老外要求加的,请参考
-
案例:
- add
- {{ add 1 2 3 4 }} -> 10
- sub
- {{ sub 4 1 }} -> 3
- mul
- {{ mul 1 2 3 4 }} -> 24
- div
- {{ div 5 2 }} -> 2.5
- mod
- {{ mod 5 2 }} -> 1
- min/max
- {{ min 5 1 }} -> 1
- {{ max 5 1 }} -> 5
- pow
- {{ pow 5 2 }} -> 25
- powf
- {{ powf 2.5 2 }} -> 6.25
- log
- {{ log 5 2 }} -> 2
- logf
- {{ logf 5 2 }} -> 约 2.32
- FormatFloat
- {{ FormatFloat "#,###.##" 2345.6789 }} -> 2,345.68
以上的模板会被渲染为:
add
- 10 -> 10
sub
- 3 -> 3
mul
- 24 -> 24
div
- 2 -> 2.5
mod
- 1 -> 1
min/max
- 1 -> 1
- 5 -> 5
pow
- 25 -> 25
powf
- 6.25 -> 6.25
log
- 2 -> 2
logf
- 2.321928094887362 -> 约 2.32
FormatFloat
- 2,345.68 -> 2,345.68
常用的时间函数
-
Sprig 常用函数
-
now
:Time
,返回当前的时间 -
date <fmt str> <Time>
:str
, 将输入的时间对象格式化为字符串-
fmt
使用2006-01-02 15:04:05
这个固定时间格式(知乎讨论) -
教你如何记忆这个 🗑️ 垃圾到 😡 爆炸的 magin number
- 首先年份固定是 2006
- 后面的月日时分秒从 01 开始依次递增到 05
- 所以标准格式为:
2006-01-02 03:04:05
- 但是
03:04:05
是 12 小时制,所以如果想用 24 小时制,要换算成2006-01-02 15:04:05
-
-
toDate <fmt str> <str>
:Time
,将一个字符串转换为一个时间对象- 注:思源内置的
parseTime
函数使用体验比这个函数要好一点
- 注:思源内置的
-
duratioin <second: int>
:Duration
, 将传入的秒数(int)转换为Duration
对象
-
思源内置时间函数
ISOWeek <Time>
:int
,返回对应的时间对应今天的第几周Weekday <Time>
:int
,返回对应的时间是星期几 ;Sunday=0, Monday=1, ..., Saturday=6
WeekdayCN <Time>
:str
,返回对应的时间是星期几;Sunday=日, Monday=一, ..., Saturday=六
WeekdayCN2 <Time>
:str
,返回对应的时间是星期几;Sunday=天, Monday=一, ..., Saturday=六
parseTime <string>
:Time
,解析传入的时间字符串,返回一个时间类型
-
Time
:Golang 的 time.Time 类型,这个类型里面有不少有用的属性可以访问-
完整函数参考:https://pkg.go.dev/time#Time
- 请查找格式为
func (t Time) Func(xxx) XXX
的 API 文档 - 这类函数(属性)都可以在模板中通过
t.Func
来调用
- 请查找格式为
-
Year
:int
-
Month
:Month
,这是一个枚举类型- 虽然显示是英文字符串,但是可以当作数值类型参与计算,例如
add 1 now.Month
- 详情参考:https://pkg.go.dev/time#Month
- 虽然显示是英文字符串,但是可以当作数值类型参与计算,例如
-
Day
:int
-
Hour
:int
-
Minute
:int
-
Second
:int
-
Sub <Time>
: 计算两个时间之间的差,返回Duration
-
Compare <Time>
:比较两个时间对象,返回int
, -1 或 0 或 1 -
AddDate <year int> <month int> <day int>
:int
,在当前的时间对象的基础上计算 N 天(月、 年)后的日期(参数可以为负数)- 注意,这个函数对月份的处理比较坑,只建议使用 year 和 day 这两个参数
- 详情参考:令人困惑的 Go time.AddDate
-
-
Duration
: Golang 的 time.Duration 类型-
完整文档见:https://pkg.go.dev/time#Duration
- 请查找格式为
func (d Duration) Func(xxx) XXX
的 API 文档 - 这类函数是可以在模板中通过
t.Func
来调用
- 请查找格式为
-
Hours
:float64
,将 Duration 转换为以小时为单位的数值 -
Minutes
:float64
,将 Duration 转换为以分钟为单位的数值 -
Seconds
:float64
,将 Duration 转换为以秒为单位的数值 -
String
:str
, 将 Duration 按照小时制转换为字符串,显示的格式为 "72h3m0.5s"
-
案例:
- now
- {{ now }}
- date
- {{ date "2006-01-02 15:04:05" now }}
- toDate
- {{ toDate "2006-01-02" "2020-01-01" }}
- duration
- 1800 second: {{ duration 1800 }}
- ISOWeek
- 第 {{ now | ISOWeek }} 周
- Weekday
- 今天是星期:
- {{ now | Weekday }} {{ now | WeekdayCN }} {{ now | WeekdayCN2 }}
- parseTime
- {{ parseTime "2020-01-01 12:00:00" }}
- Time 对象
- {{ $t := parseTime "2020-01-01 12:00:00" }}
- {{ $t.Year }}/{{ $t.Month }}/{{ $t.Day }} {{ $t.Hour }}:{{ $t.Minute }}:{{ $t.Second }}
- {{ now.Sub $t }}
- {{ $t.Compare now }}
- {{ $t.AddDate 0 0 7}}
- Duration 对象
- {{ $du := now.Sub $t }}
- {{ $du }}
- {{ $du.Hours }}; {{ $du.Minutes }}; {{ $du.Seconds }}
- {{ $du.String }}
以上的模板会被渲染为:
now
- 2024-05-06 22:08:49.9662126 +0800 CST m=+477.037392501
date
- 2024-05-06 22:08:49
toDate
- 2020-01-01 00:00:00 +0800 CST
duration
- 1800 second: 0s
ISOWeek
- 第 19 周
Weekday
- 今天是星期:
- 1 一 一
parseTime
- 2020-01-01 12:00:00 +0800 CST
Time 对象
-
- 2020/January/1 12:0:0
- 38098h8m49.9662126s
- -1
- 2020-01-08 12:00:00 +0800 CST
Duration 对象
-
- 38098h8m49.9662126s
- 38098.14721283683; 2.28588883277021e+06; 1.371533299662126e+08
- 38098h8m49.9662126s
日期计算可能是思源用户最常接触到的函数了,如果你是一个 daily note 用户,现在打开你主日记本,查看一下你日记的模板会发现它可能是这个样子:
/daily note/{{now | date "2006/01"}}/{{now | date "2006-01-02"}}
现在我们有了理论基础,不妨就这个案例来看一下这个日记模板是怎么回事:
-
{{}}
是 Golang 标准的模板语法,没什么好说的 -
now
函数返回了一个Time
对象 -
|
通过管道运算,把now
的结果传给后面,所以相当于在运行date "2006/01"
你可以把
{{now | date "2006/01"}
换成{{date "2006/01" now}}
;他们两个是完全等价的 -
date <fmt str> <Time>
是固定搭配的用法,这里"2006/01"
也是固定的用法 -
所以最后,这个模板会被渲染为
yyyy/mm
这样的格式,和前面的组合起来,就会形成/daily note/<年份>/<月份>
这样的路径字符串
分享几个日期计算的模板片段
就经验来看,在正常 md 模板中会写得比较复杂的也只有和日期计算相关的模板了。这里随便分享几个。
-
可能会用到日记里面的
.action{ $weekday := WeekdayCN now } .action{ $datestr := now | date "2006-01-02" } .action{ $datestr_sy := now | date "20060102" } 今天是 .action{$datestr} 星期.action{ $weekday } {: custom-dailynote-${ $datestr_sy }=".action{$datestr_sy}" }
-
计算这周周日、上周周六等
.action{ $weekday := Weekday now } .action{ $begSunday := now.AddDate 0 0 (mul -1 $weekday | int) } .action{ $last6 := $begSunday.AddDate 0 0 -1 } .action{ $this6 := $begSunday.AddDate 0 0 6 } - 如果以周日为一周的起始,那么本周周日为:.action{ $begSunday | date "2006-01-02" } - 上周周六为:.action{ $last6 | date "2006-01-02" } - 本周周六为:.action{ $this6 | date "2006-01-02" }
-
周规划
.action{ $week := now | ISOWeek } .action{ $weekday := now | Weekday } .action{ $monday_delta := sub $weekday 1 } .action{if eq $weekday -1} .action{ $monday_delta := 6 } .action{ end } .action{ $monday := now.AddDate 0 0 (mul -1 $monday_delta | int) } .action{ $sunday := $monday.AddDate 0 0 6 } ## 第 .action{$week} 周: .action{$monday | date "2006-01-02"} ~ .action{$sunday| date "2006-01-02"}
常用字符串操作函数
字符串操作函数在正常 md 模板里面用的没那么多,但是在数据库模板列里面可能会大量用到。
只列举了很小的一部分,完整文档见 https://masterminds.github.io/sprig/strings.html 和 https://masterminds.github.io/sprig/string_slice.html
trim <str>
:str
, 将前后的空白字符去掉repeat <int> <str>
:str
,将给定的字符串重复若干次substr <start int> <end int> <str>
:str
,提取子字符串,index 从 0 开始trunc <int> <str>
:str
,将给定的字符串按照最大长度来截断;<int>
参数可以是负数,代表从末尾反向截断abbrev <int> <str>
:str
,同样是截断字符串,但是会在后面加上一个...
contains <part str> <whole str>
:bool
,检测whole
中是否包含part
cat [<str>,]
:str
,将若干字符串拼接起来,中间通过空格间隔replace <from str> <to str> <src str>
:str
,将src
中所有的from
替换成to
- 正则表达式系列的函数(请自行翻阅文档)
join <ch str> <List[str]>
:str
,将给定的字符串列表通过ch
连接起来splitList <ch str> <src str>
:List[str]
,将给定的src
字符串根据ch
字符分割为列表
案例:
- trim
- {{ trim " aa " }} -> "aa"
- repeat
- {{ repeat 5 "12" }} -> "1212121212"
- substr
- {{ substr 1 3 "abcedfg" }} -> "bc"
- trunc
- {{ trunc 3 "abcedfg" }} -> "abc"
- {{ trunc -3 "abcedfg" }} -> "efg"
- abbrev
- {{ abbrev 5 "hello world" }} -> "he..."
- contains
- {{ contains "bb" "aabb" }} -> true
- cat
- {{ cat "1" "2" "3" }} -> "1 2 3"
- replace
- {{ replace "aa" "bb" "11aaccaa" }} -> "11bbccbb"
- join
- {{ list "hello" "siyuan" | join "?" }} -> "hello?siyuan"
- splitList
- {{ splitList "$" "foo$bar$baz" }} -> ["foo", "bar", "baz"]
渲染结果为:
trim
- aa -> "aa"
repeat
- 1212121212 -> "1212121212"
substr
- bc -> "bc"
trunc
- abc -> "abc"
- dfg -> "efg"
abbrev
- he... -> "he..."
contains
- true -> true
cat
- 1 2 3 -> "1 2 3"
replace
- 11bbccbb -> "11bbccbb"
join
- hello?siyuan -> "hello?siyuan"
splitList
- [foo bar baz] -> ["foo", "bar", "baz"]
列表操作函数
列表类函数常常会配合 queryBlocks (见后面的小节) 一同使用。
完整文档请见:https://masterminds.github.io/sprig/lists.html
-
list [<value>,]
:List
,将后面传入的参数变成一个列表(类型要相同) -
first <List>
:返回第一个列表项 -
last <List>
:返回最后一个列表项 -
append <List> <value>
:List
,在列表后面增加一个元素 -
prepend <List> <value>
:List
,在列表前面增加一个元素 -
concat [<List>,]
:List
,将多个列表合并成一个 -
reverse <List>
:List
,将列表逆序 -
has <value> <List>
:bool
,检查给定的项目是否在列表中 -
index <List> <int>
:List
,根据后面的索引值,索引列表中的内容 -
slice <List> <beg int> {<end int>}
:List
,对给定的列表进行切片slice $myList
returns[1 2 3 4 5]
. It is same asmyList[:]
.slice $myList 3
returns[4 5]
. It is same asmyList[3:]
.slice $myList 1 3
returns[2 3]
. It is same asmyList[1:3]
.slice $myList 0 3
returns[1 2 3]
. It is same asmyList[:3]
.
-
empty <List>
:bool
,检查列表是否为空 -
len <List>
:int
,获取列表的长度
- list
- {{ list 1 2 3 4 }} -> [1 2 3 4]
- {{ list "a" "b" "c" }} -> ["a" "b" "c"]
- first
- {{ first (list 1 2 3) }} -> 1
- {{ first (list "a" "b" "c") }} -> "a"
- last
- {{ last (list 1 2 3) }} -> 3
- {{ last (list "a" "b" "c") }} -> "c"
- append
- {{ append (list 1 2 3) 4 }} -> [1 2 3 4]
- {{ append (list "a" "b" "c") "d" }} -> ["a" "b" "c" "d"]
- prepend
- {{ prepend (list 1 2 3) 0 }} -> [0 1 2 3]
- {{ prepend (list "a" "b" "c") "z" }} -> ["z" "a" "b" "c"]
- concat
- {{ concat (list 1 2) (list 3 4) }} -> [1 2 3 4]
- {{ concat (list "a" "b") (list "c" "d") }} -> ["a" "b" "c" "d"]
- reverse
- {{ reverse (list 1 2 3) }} -> [3 2 1]
- {{ reverse (list "a" "b" "c") }} -> ["c" "b" "a"]
- has
- {{ has 2 (list 1 2 3) }} -> true
- {{ has "d" (list "a" "b" "c") }} -> false
- index
- {{ index (list 1 2 3 4) 2 }} -> 3
- {{ index (list "a" "b" "c" "d") 0}} -> "a"
- slice
- {{ slice (list 1 2 3 4 5) 1 3 }} -> [2 3]
- {{ slice (list "a" "b" "c" "d") 2 }} -> ["c" "d"]
- len
- {{ list 1 2 3 4 | len }} -> 4
渲染结果为
list
- [1 2 3 4] -> [1 2 3 4]
- [a b c] -> ["a" "b" "c"]
first
- 1 -> 1
- a -> "a"
last
- 3 -> 3
- c -> "c"
append
- [1 2 3 4] -> [1 2 3 4]
- [a b c d] -> ["a" "b" "c" "d"]
prepend
- [0 1 2 3] -> [0 1 2 3]
- [z a b c] -> ["z" "a" "b" "c"]
concat
- [1 2 3 4] -> [1 2 3 4]
- [a b c d] -> ["a" "b" "c" "d"]
reverse
- [3 2 1] -> [3 2 1]
- [c b a] -> ["c" "b" "a"]
has
- true -> true
- false -> false
index
- 3 -> 3
- a -> "a"
slice
- [2 3] -> [2 3]
- [c d] -> ["c" "d"]
len
- 4 -> 4
类型转换函数
在思源一个笔记软件里面,纠结数据类型这种纯编程性的问题挺古怪的——但是只要你开始用模板功能,有些时候偏偏就会出现类型兼容性问题。
例如这个例子,他的作用是计算上一个周日的日期(这里我们姑且认为每周从周一开始)。
- 首先我们使用
Weekday
函数获取今天是星期几,数值范围为 0~6 - 然后调用 AddDate 函数,减去星期数,就获得了周日的日期
{{ $weekday := Weekday now }}
{{ now.AddDate 0 0 (mul -1 $weekday) }}
不过运行的这个模板的时候会报错,无法运行:
模板解析失败:template: :2:27: executing "" at <$weekday>: wrong type for value; expected int; got int64 v3.0.12
附:顺便教一下怎么看这个报错信息。
- 关注
template: :2:27
,这代表了传入的模板的第二行,第二十七个字符的地方出现了错误- 具体的错误就是:「wrong type for value; expected int; got int64」,也就是传入的值的类型出了错。
报错的原因在于,mul
这些 Sprig 算术函数返回的类型是 int64
,而 Time.AddDate
函数接受的类型却是 int
,所以出现了类型不兼容问题。😡
为了解决这一问题,我们不得不进行使用类型转换函数,把 in64 转成 int 类型:
{{ $weekday := Weekday now }}
{{ now.AddDate 0 0 (mul -1 $weekday | int) }}
类型转换函数同样是 Sprig 提供的,文档见:https://masterminds.github.io/sprig/conversion.html
- atoi:将字符串转换为整数。
- float64:转换为 float64,即 64 位浮点数值。
- int:在系统宽度下转换为 int 整形数值。
- int64:转换为 64 位的整形数值。
- toDecimal:将 Unix 八进制转换为 int64。
- toString:转换为字符串。
- toStrings:将列表、切片或数组转换为字符串列表。
以下是一个简单的案例:
- {{atoi "11"}} -> 11
- {{float64 1}} -> 1.0
- {{int "12"}} -> 12
- {{int64 1}} -> 1
- 注意:有些函数的参数只接受 int,而有些只接受 int64,所以某些情况下不得不需要对 int 进行类型转换
- {{toDecimal "20"}} -> 16
- 八进制的 "20" 就是十进制的 16
- {{toString 120}} -> "120"
- {{toStrings (list 1 2 3 4)}} -> [1 2 3 4]
渲染为:
- 11 -> 11
- 1 -> 1.0
- 12 -> 12
- 1 -> 1
- 注意:有些函数的参数只接受 int,而有些只接受 int64,所以某些情况下不得不需要对 int 进行类型转换
- 16 -> 16
- 八进制的 "20" 就是十进制的 16
- 120 -> "120"
- [1 2 3 4] -> [1 2 3 4]
逻辑运算函数
逻辑运算函数,主要是在 if
这类流程控制语句内需要用到。比如这个例子当中,eq 就是一个逻辑运算函数,他用来比较传入的两个参数是否相等(equal 的简写)。
.action{ if eq 1 2 }
1
.action{ else }
2
.action{ end }
在更加实际的例子中,可能我们要写好几个逻辑运算,建议将每个逻辑运算囊括在 () 当中来保证运算的正确性。
.action{ if or (eq 1 2) (ne 2 3) }
1
.action{ else }
2
.action{ end }
逻辑运算部分可以分为两大类:布尔运算类和比较运算类。这里的参数类型 <interface{}>
表示是任意类型。
-
布尔运算
and [<interface{}>,]
:interface{}
,如果所有参数都为真,则返回最后一个参数;否则返回第一个为假的参数or [<interface{}>,]
:interface{}
,如果任一参数为真,则返回第一个为真的参数;否则返回最后一个参数not <interface{}>
:bool
,对单个参数进行逻辑非运算;如果参数为真则返回假,为假则返回真
-
比较运算:对两个类型进行比较,注意这里的参数必须要是同样的类型
eq <interface{}>, <interface{}>
:bool
,判断两个参数是否相等;相等返回真,不等返回假ne <interface{}>, <interface{}>
:bool
,判断两个参数是否不相等;不相等返回真,相等返回假lt <interface{}>, <interface{}>
:bool
,比较两个参数,如果第一个小于第二个,则返回真,否则返回假le <interface{}>, <interface{}>
:bool
,比较两个参数,如果第一个小于等于第二个,则返回真,否则返回假gt <interface{}>, <interface{}>
:bool
,比较两个参数,如果第一个大于第二个,则返回真,否则返回假ge <interface{}>, <interface{}>
:bool
,比较两个参数,如果第一个大于等于第二个,则返回真,否则返回假
在常规的逻辑运算函数之外,还有必要介绍一类列表、对象判断类函数,这部份函数由 Default Functions | sprig 提供
-
empty <interface{}>
:bool
,判断给定的对象是否为空- 在思源中,最常见的用法是判断一个列表是不是空的
-
all [<interface{}>,]
:bool
,判断给定一系列对象,是否每个都是非空的 -
any [<interface{}>,]
:bool
,判断给定一系列对象,是否存在某一个是非空的
案例如下:
- and
- {{ and true true }} -> true
- {{ and true false }} -> false
- {{ and 1 2.3 "str" }} -> "str"
- or
- {{ or false false }} -> false
- {{ or true false }} -> true
- not
- {{ not true }} -> false
- {{ not 0 }} -> true
- eq
- {{ eq 2 3 }} -> false
- {{ eq "a" "a" }} -> true
- ne
- {{ ne 4 3 }} -> true
- {{ ne 2 2 }} -> false
- lt
- {{ lt 4 3 }} -> false
- {{ lt "a" "b" }} -> true
- le
- {{ le 2 2 }} -> true
- {{ le 4 3 }} -> false
- gt
- {{ gt 5 3 }} -> true
- {{ gt 2 2 }} -> false
- ge
- {{ ge 3 3 }} -> true
- {{ ge 1 4 }} -> false
- empty
- {{ list 1 | empty }} -> false
- {{ list | empty }} -> true
and
- true -> true
- false -> false
- str -> "str"
or
- false -> false
- true -> true
not
- false -> false
- true -> true
eq
- false -> false
- true -> true
ne
- true -> true
- false -> false
lt
- false -> false
- true -> true
le
- true -> true
- false -> false
gt
- true -> true
- false -> false
ge
- true -> true
- false -> false
empty
- false -> false
- true -> true
附:使用 ternary 函数计算内联条件
我们上面谈到了最常用的 if 条件语句:
.action{ if eq 1 2 }
1
.action{ else }
2
.action{ end }
如果对编程比较有经验的人应该知道,很多语言中都支持计算内联条件,以 js 为例子
const x = (1 == 2)? 1 : 2;
在模板函数中,这个内联条件计算可以用 ternary
函数来完成,基本用法为:ternary <arg1> <arg2> <cond bool>
:
.action{ ternary "Show 1" "Show 2" (eq 1 2) }
思源模板片段特殊函数
有几个思源内置的特殊模板函数在文档里有所介绍,这几个函数**==只能用在 md 模板文件==当中**。
title
:该变量用于插入当前文档名。比如模板内容为# .action{.title}
,则调用后会以一级标题语法插入到当前文档内容中id
:该变量用于插入当前文档 IDname
:该变量用于插入当前文档命名alias
:该变量用于插入当前文档别名
这四个实际上是在「访问正在插入模板的容器文档的一些属性」。
我们可以把下面这个例子保存到 template 目录下的 md 模板文件当中。
这个是当前插入文档块的标题:.action{.title}
这个是当前插入文档块的ID:.action{.id}
这个是当前插入文档块的命名:.action{.name}
这个是当前插入文档块的别名:.action{.alias}
然后在某个文档当中插入这个模板,你就会发现这几个字段被替代为被插入的文档块的具体的属性。
这个是当前插入文档块的标题:思源模板片段新人指南
这个是当前插入文档块的 ID:20231014181953-m94pl9u
这个是当前插入文档块的命名:
这个是当前插入文档块的别名:
注意这里使用的时候必须前面要加上 .
,也就是 .title
而非 title
。至于为什么要加 .
,有兴趣可以参考这个:Template · Go 语言中文文档。
SQL 查询函数
思源还额外提供了两个模板函数,用于做 SQL 查询。
如果你对思源的 SQL 功能不了解,请阅读:思源 SQL 新人指南:SQL 语法 + Query + 模板
-
queryBlocks
:该函数用于查询数据库,返回值为 blocks 列表,请参考下面的例子- ⭐ 这个更常用!
-
querySpans
:该函数用于查询数据库,返回值为 spans 列表,请参考下面的例子- 🤷♂️ 这个很少用。
这两个模板函数的返回值都是块的列表。
queryBlocks
queryBlocks
可能是最常用的,他的作用就是在 blocks 表中做查询。
.action{$blocks := queryBlocks "select * from blocks where 1 limit 1"}
返回结果是列表,这里取出第一个元素:.action{ $b := first $blocks}
块的 ID .action{$b.ID}
块的路径 .action{$b.Path}
完整的块的内容:
```json
.action{toPrettyJson $b }
```
我们通过 toPrettyJson
函数(见 Default Functions | sprig)将结果变成一个 Json,可以发现返回的就是一个 Block 对象。
返回结果是列表,这里取出第一个元素:
块的 ID 20220316145831-f0lgt06
块的路径 /20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06.sy
完整的块的内容:{ "ID": "20220316145831-f0lgt06", "ParentID": "", "RootID": "20220316145831-f0lgt06", "Hash": "d7b97e4", "Box": "20220305173526-4yjl33h", "Path": "/20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06.sy", "HPath": "/daily note/2022/03", "Name": "", "Alias": "", "Memo": "", "Tag": "", "Content": "03", "FContent": "03", "Markdown": "", "Length": 2, "Type": "d", "SubType": "", "IAL": "{: custom-sy-readonly=\"true\" id=\"20220316145831-f0lgt06\" title=\"03\" type=\"doc\" updated=\"20220316145831\"}", "Sort": 0, "Created": "20220316145831", "Updated": "20220316145831" }
在这里我们使用了如下的写法:
.action{$blocks := queryBlocks "select * from blocks where 1 limit 1"}
但是某些情况下,可能 SQL 里面的需要插入一些变量,这种情况下,可以使用 ?
占位符来插入变量:
.action{$id := "20240507145154-4g8hqau"}
.action{$blocks := queryBlocks "select * from blocks where id='?'" $id}
querySpans
querySpans
则很少用,他是用来给 spans 表做查询用的
.action{$spans := querySpans "select * from spans where 1 limit 1"}
```json
.action{toJson $spans }
```
[{"ID":"20240420235208-65m9ro6","BlockID":"20220324170238-169kwsw","RootID":"20220324170216-lp8jviy","Box":"20220305173526-4yjl33h","Path":"/20220316145830-u0u6srg/20220316145830-x6kvftp/20220316145831-f0lgt06/20220324170216-lp8jviy.sy","Content":"随想录","Markdown":"((20220320164548-ienx5sl '随想录'))","Type":"textmark block-ref","IAL":""}]
案例:链接到昨天的日记
querySpans 且不谈,配合 queryBlocks 我们可以在模板中玩一些奇妙的花活。本小节作为模板语法部分的收尾,给大家介绍一个有趣的案例。
前两天我看有人在论坛里问:日记模板怎样设置可以在今天日记页面中自动添加昨天的日记链接?
他提得需求就完全可以通过 queryBlocks 来完成。
要实现这个功能,必须知道两个前置知识:
- 在思源里,日记文档会自动添加
custom-dailynote-<yyyymmdd>
属性;比如 2024-05-01 的日记,会自动添加文档属性custom-dailynote-20240501
- 思源中,超链接的格式为
siyuan://blocks/<块 ID>
有了这两个知识,我们就有大致的实现思路了:
- 获取昨天的日期
- 根据昨天的日期构建日记文档属性
- 查询符合文档属性的文档
- 如果查询到,就去除文档的 ID 并构建块链接
首先要获得 yyyymmdd
属性,这个我在「常用的时间函数」当中已经给出写法了:
.action{ $datestr_sy := now | date "20060102" }
{: custom-dailynote-${ $datestr_sy }=".action{$datestr_sy}" }
这里我们要把 now
替换成昨天,改成这样以下这样;其中用到了 list、join 两个函数,我在前面也介绍过。
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" }
.action{ $attr := list "custom-dailynote-" $datestr_sy | join "" }
接下来我们就要在模板中调用 queryBlock 来查询昨天的日记:
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" }
.action{ $attr := list "custom-dailynote-" $datestr_sy | join "" }
.action{ $docs := queryBlocks "select * from blocks where type='d' and ial like '%custom-dailynote-20240506%' limit 1"}
获得了 $docs
我们需要做以下的工作:
- 首先判断列表是否为空(用 empty 函数),因为很可能昨天没有写日记
- 如果不为空,就使用
first
函数取出第一个文档元素 - 通过
.ID
获取文档的 ID,并构造 markdown 链接
最后完整的模板内容如下:
.action{ $datestr_sy := now.AddDate 0 0 -1 | date "20060102" }
.action{ $attr := list "custom-dailynote-" $datestr_sy | join "" }
.action{ $docs := queryBlocks "select * from blocks where type='d' and ial like '%custom-dailynote-20240506%' limit 1" }
.action{ if not (empty $docs) }
.action{ $doc := first $docs }
[.action{$doc.Content}](siyuan://blocks/.action{ $doc.ID })
.action{ end }
数据库中的模板列
思源的数据库中,有一种类型的列叫做「模板列」,通过模板列,我们可以让数据库发挥出更加强大的功能。
口说无凭,来看一个数据库的效果:
在这个数据库当中,「评级」是一个模板列,他会根据左边打分的数值,自动调整显示为不同的星星数量。那么这是怎么做到的呢?
-
首先新建评级列,将它设置为一个模板列
-
编辑模板为
.action{ $scale := (div .打分 20) } .action{ repeat (int $scale) "⭐" }
看起来非常简单!你应该能记得,我们之前讨论过这里用到的三个函数:
div
:进行除法运算,并将结果赋值给$scale
变量- 由于
div
的结果类型为int64
所以通过int
函数把他转为整型 - 通过调用
repeat
函数,根据$scale
变量的数值来重复显示 ⭐ 符号
插件测试样例,注:由于这里是在插件当中测试,无法访问
.打分
属性,所以用常量 50 来替换以完成测试。
当然以上都不是重点,这里的重点是—— .打分
是什么,为什么我们可以访问他?
.
对象
也许你还记得,.
符号在此前也出现过很多次。比如之前的 now.Year
就表示:
- 通过函数
now
返回的对象Time
- 对对象
Time
使用.
获取他的属性Year
同样在这里 .
符号也表示获取属性。但是由于之前没有指定被索引的对象,所以这里使用了传入模板的默认数据对象——在思源数据库中,他就**对应了模板列单元所在的****==行本身==**。
例如一个数据库拥有 日期
、分数
、备注
这些列,我们就可以通过 .日期
、.分数
、.备注
在模板列中访问他们。
那么除了列本身,还有可以访问的对象吗?
当然有!如果你想知道更加细节的可访问字段,这里有一个简单的技巧:
- 在数据库中新建一个模板列
- 将模板设置为
.action{toPrettyJson .}
- 将模板列设置为「换行」
然后你就可以在模板列中看到本数据库中的所有可访问字段了,比如通过 .created
访问行创建的时间。
但是注意,并不是所有的属性都可以通过 .
来访问,比如如果我们尝试访问一个名为 .custom-b
的对象,那么会喜提一个报错——因为 custom-b 不符合属性的命名规范。
对于这种情况,可以通过 index . "name"
来对属性进行访问,比如这个例子中,我们填写 .action{ index . "custom-b"}
就可以访问 custom-b
属性的值了。
附注:
模板使用关联或汇总时,填充值为数组,所以可能需要使用
index .汇总 0
来访问第一个值,或者使用range
迭代所有值。
两种主键
思源的数据库中的主键可以分为两种:
- 普通的文本主键
- 被绑定到一个块的主键
为了方便测试,我们现在新建一个测试用的块,并填写命名、属性、备注还有自定义属性:
以下是不同的两行内部属性的差异:
-
普通文本主键只有 id、update、created、主键等属性
-
绑定了块的主键,还多出来
- 数据库相关的属性,如 av-names、custom-avs 等
- 块的内置属性,如命名、别名等
- 自定义属性,如 "custom-a"
-
绑定了块的主键,其 ID 指向了被绑定的块,而普通文本主键的 ID 是它自身的 ID
-
注意:普通的文本主键的 ID 并非指向一个块,所以如果你尝试用 SQL 来查询这个 ID 对应的块,是不会得到任何有意义的结果的!
和 queryBlocks 结合
对于主键绑定了块的数据库而言,由于 ID 属性和对应的块打通了沟通渠道,所以把模板列和 queryBlocks 方法结合玩一些更加花哨的功能。
这里给出一个简单的案例:自动获取绑定块的文档的标题
-
首先使用 queryBlocks 执行 SQL 查询获取对应的文档块,SQL 语句的设计如下:
select * from blocks where id in (select root_id from blocks where id='<id>')
-
通过 if 语句过滤掉不存在文档块的情况
- 通过
first
函数取出第一个文档块 - 然后获取它的 Content 字段
- 通过
-
对于不存在文档块的情况,简单输出一个「无」
.action{ $blocks := queryBlocks "select * from blocks where id in (select root_id from blocks where id='?')" .id}
.action{ if not (empty $blocks)}
.action{ (first $blocks).Content }
.action{ else }
无
.action{ end }
效果如下:
论坛里有一个帖子,询问:数据库一些列的值能否自动获取文档里的数据。其实解决思路和上面的是一样的:
- 获取绑定块的 ID
- 构造 SQL,查询到想要的内容
- 读取 queryBlocks 的结果,并写入数据库模板列中
和 html 标签结合
有一个很重要,但是可能很少人知道的事情:数据库的文本列里面是==可以直接填写 html 元素的==。
举一个例子,我们把如下的 html 粘贴到一个文本列当中:
<div style="
background: conic-gradient(#4CAF50 0%, #4CAF50 75%, #ddd 75%);
border-radius: 50%;
width: 40px;
height: 40px;
position: relative;">
<span style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;">
75%
</span>
</div>
然后,这个元素就会被完整地渲染在数据库单元内:
坏消息是:当你再次点击编辑的时候,里面就只剩下纯文本了。
🤨 更坏的消息:可能未来某个版本内,文本列不会再支持插入 html 了。
好消息是:模板列里面也支持使用自定义的 html 元素。我们不妨再做一个测试:
-
新建一个数值列“比例”,取值范围从 0 ~ 100
-
将如下的模板粘贴到一个模板列当中,他做的事情很简单:读取「比例」的数值,然后替换到 div 元素当中
.action{ $portion := index . "比例"} <div style=" background: conic-gradient(#4CAF50 0%, #4CAF50 .action{$portion}%, #ddd .action{$portion}%); border-radius: 50%; width: 40px; height: 40px; position: relative;"> <span style=" position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px;"> .action{$portion}% </span> </div>
做完以上的工作,数据库就会根据 "比例" 这一列的数值来绘制模板列的样式,从而显示出不同的效果出来:
奇奇怪怪的玩法(存在风险,慎用)
拓展性花活,普通用户慎用。
自定义 html + 可执行的 js 是存在安全风险的,这里只是通过这个例子告诉大家数据库可以做到这种程度。
如果你不能百分百确定你在干什么,就不要这么玩。
先给大家看一个神奇的东西:
在前面我们提到过,数据库的文本实际上是可以直接插入 html 标签的——那既然可以插入 html 标签,那执行一下 js 代码也是非常合理的对吧。
现在我来解释一下上面这个效果的实现方案:
-
首先将数据库的主键绑定到一个块上——从而保证使用
.id
能访问到块的 id -
新建一个 css 文本列,用来填写具体的 css 属性表
-
新建一个模板列,里面填写如下内容
<button style="margin: 3px;" class="b3-button" onclick='event.stopImmediatePropagation(); runJs.api.setBlockAttrs(".action{.id}", {style: ".action{index . "css"}"});' > 添加样式 </button>
我们来分析一下这个模板:
-
<button style="margin: 3px;" class="b3-button" />
构建了一个按钮元素,b3-button
是思源的内置 class 名称 -
关键是 onclick 元素,我们在 onclick 当中直接填写我们要执行的代码:
-
event.stopImmediatePropagation();
- 这行代码是为了阻止思源默认的数据表行为,如果去掉了以后,思源就会弹出一个编辑框,而不会触发点击按钮的事件了
-
runJs.api.setBlockAttrs
-
这个其实是安装了
RunJs
插件后,插件暴露在外部的一个接口 -
setBlockAttrs 是思源的内核 API,用于为块设置块属性
-
你不一定要安装 runJs 插件,也可以在 JS 代码片段中添加自己想要使用的代码功能
-
-
setBlockAttrs 接受两个参数:块的 ID,具体的块属性
-
块 ID:通过模板功能的
.action{.id}
获取 -
块属性:由于我们要设置内联样式属性,所以 API 的 payload body 填写为
{style: "<具体css代码>"}
- 使用
.action{index . "css"}
获取 css 列的内容,填充到 payload body 中
- 使用
-
-
思源 Markdown 块语法
严格来说,思源的 Markdown 块语法和模板语法没有半毛钱关系。但是由于在 md 模板文件里面经常需要使用 md 块语法,所以有必要在这里一起介绍一下。
我们都知道思源是以块为单位的,同时思源一定程度上兼容了 markdown 语法格式(例如在模板文件中)——那么问题来了,markdown 原始的语法就是个瘸子,根本不存在块的概念,更不用提块属性这种概念了——这可怎么办呢。
对于最普通的情况来说来讲,一个 markdown 内置的语法元素,例如标题、列表、引述等等,也会被转化为一个单独的块。
而对于更加复杂的情况,思源实现了一种基于 kramdown 的 markdown 方言。通过这个方言我们可以在一个普通的 markdown 纯文本文件中定义块及其内部的属性。
- 思源的 markdown 这些拓展属性语法,不仅仅可以用在模板的 md 文件里, 还可以直接用在编辑器中。
- 在阅读下面的示例的时候,你可以把给出的 md 模板样例复制之后直接粘贴到思源的编辑器中,思源同样能够正确识别定义的块属性。
块属性声明语法
思源可以通过 {: }
这种内联样式表 (简称:ial) 的形式来声明一个具备特定属性的块,内联样式表的基本语法格式为如下,注意括号两侧必须留出空格。
{: <key1>="<value>1" <key1>="<value>1" }
例如,你可以把以下的文本填入 templates
下的 Test.md 文件:
这是一个简单的段落,但是我会声明「备注」内联样式和「custom-a」自定义属性。
{: memo="这是一个块" custom-a="a" }
而将这个模板应用到思源正文后,就会创建一个填充了备注和 custom-a 属性的块。
再比如,在使用了 callout (由 Savor 主题以及 callout 插件提供)功能后,想要通过模板快速插入一个 callout——通过观察发现 callout 样式主要依赖于对一个引述块添加 custom-b
属性,所以我们可以首先在 Test.md 中编写引述块的 markdown 语法,然后在下方紧贴着添加 {: }
属性表:
> Meta
> - 🚩 文档类型:主题文档 | 事件记录
> - ⭐ 相关主题:
> - 📝 基本介绍:
{: custom-b="info" }
注 :可以使用的块属性包括了块的内置属性和自定义属性
- 可以使用内置样式包括:id, name, alias, bookmark, memo, style
- 在设置自定义样式的时候,不要忘了添加
custom-
前缀
Special Case: ID 属性
我们可以在 ial 表中定义 ID 属性,比如这样:
这是一个块,你猜他的 ID 会是什么?
{: id="20231004221035-p9r4sh7"}
然而,在 ial 表中定义的 ID 属性并不会真正应用到编辑器的块中——也就是说就算使用了上述的模板,新创建的块的 ID 并不会等于 "20231004221035-p9r4sh7",而是思源自己生成的新的块 ID。
id 属性在 ial 中唯一的作用是作为 ial 中的占位符来声明一个块——因为一个空的 ial 表是无效的。
听起来像是脱裤子放屁——确实,在大部分情况下,这个 trick 都没有什么用。
但是在处理复杂容器块的时候,我们会不得不使用这个 trick 来区分容器块和内部的内容块。这里给一个例子:
假定:我们需要编写一个内容为空的列表块,并给这个列表块添加
style="border: 1px solid blue;"
属性(也就是添加外边框的内联 css 样式)。请问要怎么办呢?
你可能会写出如下的模板样式
-
{: style="background: red;" }
但是以上的模板是无效的。
原因在于,思源中的列表块是一个非常复杂的嵌套对象,一个最简单的单个项目的列表,实际上也包含了三个部分:
- 最内部的段落块
- 段落块外部的列表项块
- 列表项从属的列表块
而在以上的模板中,思源无法识别 {: style="background: red;" }
到底是应用在哪个块上面。
在这种情况下,我们必须要通过类似 {: id="20231004221035-p9r4sh7"}
这样的样式表,来明确声明三层块结构,也就是要写成这样子:
- {: id="202001010000-abcdefg"}
{: id="202001010000-abcdefg"}
{: style="border: 1px solid blue;" }
另外虽然 id
的值实际上没有什么卵用,但是在填写的时候必须要遵守 ID 的格式规范,否则无法被正常识别:
yyyymmddHHMMSS-<7位字符或数字>
诸如 "202001010000-abcdefg"、"202001010000-1234567"这些都是可以的,而 "123" 这种是不行的。
超级块排版
思源支持通过超级块来进行复杂排版,这个排版同样可以通过拓展的 markdown 语法来定义:
-
多行超级块(垂直布局)
{{{row <内部内容> }}}
-
多列超级块(水平布局)
{{{col <内部内容> }}}
具体编写的时候,只需要把 < 内部内容 > 当中替换为正常的块定义即可。然后渲染的时候,内部内容就会按照垂直或者水平布局被囊括在一个新的超级块当中。
这里给一个稍微复杂一点的样例,如果你把这个样例理解了,超级块排版的布局基本上就掌握了。
{{{col
{{{row
### TODO:高优先
- [x]
}}}
{: style="border: 2px solid blue;"}
{{{row
### TODO:低优先
- [x]
}}}
{: style="border: 2px solid blue;"}
}}}
{: style="border: 2px solid red; padding: 10px;"}
插入编辑器后,样式如下:
-
最外部是一个水平布局的超级块,内部有两列垂直布局的超级块
-
内部的两个列中,各自都是一个垂直布局的超级块,包括
- 最上面一个标题块
- 最下面一个任务块
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于