Support code block highlighting template syntax and export code block templates as paragraphs (#15345)

*  feat: 优化模板编辑体验,支持使用代码块来存储模板语法

*  feat: 支持对template代码块进行高亮

- 渲染.action 块和{{}}块规则
- 渲染Markdown块
- 思源块属性设置语法

* Update third-languages.js

* Update third-languages.js

* Update third-languages.js

* Update third-languages.js

* Update third-languages.js:块属性支持高亮.action{}语法,调整relevance

* 支持渲染queryBlocks的sql语句和嵌入块sql语句

* Update third-languages.js

补充变量高亮:getHPathByID|getBlock|statBlock|runeCount|wordCount|toPrettyJson|

*  feat: 优化模板编辑体验,支持使用代码块来存储模板语法 #15345

导出时,增加识别`siyuan-template`代码块为模板

*  feat: 优化模板编辑体验,支持使用代码块来存储模板语法 #15345

代码块语言新增`siyuan-template`

* Update template.go

---------

Co-authored-by: D <845765@qq.com>
This commit is contained in:
Achuan-2 2025-07-29 21:36:34 +08:00 committed by GitHub
parent 2994969286
commit 009a68aa7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 256 additions and 1 deletions

View file

@ -758,7 +758,7 @@ export abstract class Constants {
// common: "bash", "c", "csharp", "cpp", "css", "diff", "go", "xml", "json", "java", "javascript", "kotlin", "less", "lua", "makefile", "markdown", "objectivec", "php", "php-template", "perl", "plaintext", "python", "python-repl", "r", "ruby", "rust", "scss", "sql", "shell", "swift", "ini", "typescript", "vbnet", "yaml", "properties", "1c", "armasm", "avrasm", "actionscript", "ada", "angelscript", "accesslog", "apache", "applescript", "arcade", "arduino", "asciidoc", "aspectj", "abnf", "autohotkey", "autoit", "awk", "basic", "bnf", "dos", "brainfuck", "cal", "cmake", "csp", "cos", "capnproto", "ceylon", "clean", "clojure", "clojure-repl", "coffeescript", "coq", "crystal", "d", "dns", "dart", "delphi", "dts", "django", "dockerfile", "dust", "erb", "elixir", "elm", "erlang", "erlang-repl", "excel", "ebnf", "fsharp", "fix", "flix", "fortran", "gcode", "gams", "gauss", "glsl", "gml", "gherkin", "golo", "gradle", "groovy", "haml", "hsp", "http", "handlebars", "haskell", "haxe", "hy", "irpf90", "isbl", "inform7", "x86asm", "jboss-cli", "julia", "julia-repl", "ldif", "llvm", "lsl", "latex", "lasso", "leaf", "lisp", "livecodeserver", "livescript", "mel", "mipsasm", "matlab", "maxima", "mercury", "axapta", "routeros", "mizar", "mojolicious", "monkey", "moonscript", "n1ql", "nsis", "nestedtext", "nginx", "nim", "nix", "node-repl", "ocaml", "openscad", "ruleslanguage", "oxygene", "pf", "parser3", "pony", "pgsql", "powershell", "processing", "prolog", "protobuf", "puppet", "purebasic", "profile", "q", "qml", "reasonml", "rib", "rsl", "roboconf", "sas", "sml", "sqf", "step21", "scala", "scheme", "scilab", "smali", "smalltalk", "stan", "stata", "stylus", "subunit", "tp", "taggerscript", "tcl", "tap", "thrift", "twig", "vbscript", "vbscript-html", "vhdl", "vala", "verilog", "vim", "wasm", "mathematica", "wren", "xl", "xquery", "zephir", "crmsh", "dsconfig", "graphql", // common: "bash", "c", "csharp", "cpp", "css", "diff", "go", "xml", "json", "java", "javascript", "kotlin", "less", "lua", "makefile", "markdown", "objectivec", "php", "php-template", "perl", "plaintext", "python", "python-repl", "r", "ruby", "rust", "scss", "sql", "shell", "swift", "ini", "typescript", "vbnet", "yaml", "properties", "1c", "armasm", "avrasm", "actionscript", "ada", "angelscript", "accesslog", "apache", "applescript", "arcade", "arduino", "asciidoc", "aspectj", "abnf", "autohotkey", "autoit", "awk", "basic", "bnf", "dos", "brainfuck", "cal", "cmake", "csp", "cos", "capnproto", "ceylon", "clean", "clojure", "clojure-repl", "coffeescript", "coq", "crystal", "d", "dns", "dart", "delphi", "dts", "django", "dockerfile", "dust", "erb", "elixir", "elm", "erlang", "erlang-repl", "excel", "ebnf", "fsharp", "fix", "flix", "fortran", "gcode", "gams", "gauss", "glsl", "gml", "gherkin", "golo", "gradle", "groovy", "haml", "hsp", "http", "handlebars", "haskell", "haxe", "hy", "irpf90", "isbl", "inform7", "x86asm", "jboss-cli", "julia", "julia-repl", "ldif", "llvm", "lsl", "latex", "lasso", "leaf", "lisp", "livecodeserver", "livescript", "mel", "mipsasm", "matlab", "maxima", "mercury", "axapta", "routeros", "mizar", "mojolicious", "monkey", "moonscript", "n1ql", "nsis", "nestedtext", "nginx", "nim", "nix", "node-repl", "ocaml", "openscad", "ruleslanguage", "oxygene", "pf", "parser3", "pony", "pgsql", "powershell", "processing", "prolog", "protobuf", "puppet", "purebasic", "profile", "q", "qml", "reasonml", "rib", "rsl", "roboconf", "sas", "sml", "sqf", "step21", "scala", "scheme", "scilab", "smali", "smalltalk", "stan", "stata", "stylus", "subunit", "tp", "taggerscript", "tcl", "tap", "thrift", "twig", "vbscript", "vbscript-html", "vhdl", "vala", "verilog", "vim", "wasm", "mathematica", "wren", "xl", "xquery", "zephir", "crmsh", "dsconfig", "graphql",
// third: "yul", "solidity", "abap", "hlsl", "gdscript" // third: "yul", "solidity", "abap", "hlsl", "gdscript"
public static readonly ALIAS_CODE_LANGUAGES: string[] = [ public static readonly ALIAS_CODE_LANGUAGES: string[] = [
"js", "ts", "html", "toml", "c#", "bat", "js", "ts", "html", "toml", "c#", "bat", "siyuan-template"
]; ];
public static readonly SIYUAN_RENDER_CODE_LANGUAGES: string[] = [ public static readonly SIYUAN_RENDER_CODE_LANGUAGES: string[] = [
"abc", "plantuml", "mermaid", "flowchart", "echarts", "mindmap", "graphviz", "math" "abc", "plantuml", "mermaid", "flowchart", "echarts", "mindmap", "graphviz", "math"

View file

@ -133,3 +133,236 @@ hljs.registerLanguage("hlsl",(()=>{"use strict";const e={className:"number",
// https://github.com/highlightjs/highlightjs-gdscript // https://github.com/highlightjs/highlightjs-gdscript
hljs.registerLanguage("gdscript",function(){"use strict";var e=e||{};function r(e){return{aliases:["godot","gdscript"],keywords:{keyword:"and in not or self void as assert breakpoint class class_name extends is func setget signal tool yield const enum export onready static var break continue if elif else for pass return match while remote sync master puppet remotesync mastersync puppetsync",built_in:"Color8 ColorN abs acos asin atan atan2 bytes2var cartesian2polar ceil char clamp convert cos cosh db2linear decimals dectime deg2rad dict2inst ease exp floor fmod fposmod funcref get_stack hash inst2dict instance_from_id inverse_lerp is_equal_approx is_inf is_instance_valid is_nan is_zero_approx len lerp lerp_angle linear2db load log max min move_toward nearest_po2 ord parse_json polar2cartesian posmod pow preload print_stack push_error push_warning rad2deg rand_range rand_seed randf randi randomize range_lerp round seed sign sin sinh smoothstep sqrt step_decimals stepify str str2var tan tanh to_json type_exists typeof validate_json var2bytes var2str weakref wrapf wrapi bool int float String NodePath Vector2 Rect2 Transform2D Vector3 Rect3 Plane Quat Basis Transform Color RID Object NodePath Dictionary Array PoolByteArray PoolIntArray PoolRealArray PoolStringArray PoolVector2Array PoolVector3Array PoolColorArray",literal:"true false null"},contains:[e.NUMBER_MODE,e.HASH_COMMENT_MODE,{className:"comment",begin:/"""/,end:/"""/},e.QUOTE_STRING_MODE,{variants:[{className:"function",beginKeywords:"func"},{className:"class",beginKeywords:"class"}],end:/:/,contains:[e.UNDERSCORE_TITLE_MODE]}]}}return e.exports=function(e){e.registerLanguage("gdscript",r)},e.exports.definer=r,e.exports.definer||e.exports}()); hljs.registerLanguage("gdscript",function(){"use strict";var e=e||{};function r(e){return{aliases:["godot","gdscript"],keywords:{keyword:"and in not or self void as assert breakpoint class class_name extends is func setget signal tool yield const enum export onready static var break continue if elif else for pass return match while remote sync master puppet remotesync mastersync puppetsync",built_in:"Color8 ColorN abs acos asin atan atan2 bytes2var cartesian2polar ceil char clamp convert cos cosh db2linear decimals dectime deg2rad dict2inst ease exp floor fmod fposmod funcref get_stack hash inst2dict instance_from_id inverse_lerp is_equal_approx is_inf is_instance_valid is_nan is_zero_approx len lerp lerp_angle linear2db load log max min move_toward nearest_po2 ord parse_json polar2cartesian posmod pow preload print_stack push_error push_warning rad2deg rand_range rand_seed randf randi randomize range_lerp round seed sign sin sinh smoothstep sqrt step_decimals stepify str str2var tan tanh to_json type_exists typeof validate_json var2bytes var2str weakref wrapf wrapi bool int float String NodePath Vector2 Rect2 Transform2D Vector3 Rect3 Plane Quat Basis Transform Color RID Object NodePath Dictionary Array PoolByteArray PoolIntArray PoolRealArray PoolStringArray PoolVector2Array PoolVector3Array PoolColorArray",literal:"true false null"},contains:[e.NUMBER_MODE,e.HASH_COMMENT_MODE,{className:"comment",begin:/"""/,end:/"""/},e.QUOTE_STRING_MODE,{variants:[{className:"function",beginKeywords:"func"},{className:"class",beginKeywords:"class"}],end:/:/,contains:[e.UNDERSCORE_TITLE_MODE]}]}}return e.exports=function(e){e.registerLanguage("gdscript",r)},e.exports.definer=r,e.exports.definer||e.exports}());
// https://github.com/siyuan-note/siyuan/pull/15345
hljs.registerLanguage('template', function (hljs) {
const markdownRules = hljs.getLanguage('markdown') || { contains: [] };
const goRules = hljs.getLanguage('go') || { contains: [] };
// 内置函数规则
const BUILT_IN_FUNCTIONS = {
className: 'built_in',
begin: /\b(queryBlocks|querySpans|querySQL|getHPathByID|getBlock|statBlock|runeCount|wordCount|toPrettyJson|parseTime|Weekday|WeekdayCN|WeekdayCN2|ISOWeek|pow|powf|log|logf|FormatFloat|now|date|toDate|duration|AddDate|Sub|sub|add|mul|mod|div|min|max|Compare|Year|Month|Day|Hour|Minute|Second|Hours|Minutes|Seconds|String|trim|repeat|substr|trunc|abbrev|contains|cat|replace|join|splitList|list|first|last|append|prepend|concat|reverse|has|index|slice|len|atoi|float64|int|int64|toDecimal|toString|toStrings|dict|get)\b/,
relevance: 10
};
// 变量规则 - 以$开头,不包括.号
const VARIABLE_RULE = {
className: 'variable',
begin: /\$[a-zA-Z_][a-zA-Z0-9_]*/,
relevance: 10
};
// 关键字和操作符
const KEYWORDS_OPERATORS = {
className: 'keyword',
begin: /\b(if|else|end|range|not|and|or|eq|ne|lt|le|gt|ge|empty|all|any|ternary|true|false)\b/,
relevance: 10
};
// SQL 内容的通用规则
const SQL_CONTENT_RULES = [
// SQL 关键字 - 大小写不敏感
{
className: 'keyword',
begin: /\b(SELECT|FROM|WHERE|AND|OR|ORDER|BY|GROUP|HAVING|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|INDEX|TABLE|DATABASE|LIKE|IN|NOT|IS|NULL|DISTINCT|LIMIT|OFFSET|JOIN|INNER|LEFT|RIGHT|OUTER|ON|AS|COUNT|SUM|AVG|MIN|MAX|CASE|WHEN|THEN|ELSE|END)\b/i,
relevance: 10
},
// SQL 函数 - 大小写不敏感
{
className: 'built_in',
begin: /\b(COUNT|SUM|AVG|MIN|MAX|LENGTH|UPPER|LOWER|SUBSTR|TRIM|REPLACE|CONCAT)\b/i,
relevance: 8
},
// SQL 中的字符串 (单引号)
{
className: 'string',
begin: /'/,
end: /'/,
contains: [{ begin: /''/ }]
},
// 数字
{
className: 'number',
begin: /\b\d+(\.\d+)?\b/,
relevance: 0
},
// 思源笔记特定的字段
{
className: 'attr',
begin: /\b(id|parent_id|root_id|hash|box|path|hpath|name|alias|memo|tag|content|fcontent|markdown|length|type|subtype|ial|sort|created|updated)\b/,
relevance: 8
},
// 问号占位符
{
className: 'symbol',
begin: /\?/,
relevance: 5
},
// .action{} 嵌套在 SQL 中
{
begin: /(\.action\{)/,
end: /(\})/,
beginScope: 'selector-pseudo',
endScope: 'selector-pseudo',
contains: [VARIABLE_RULE, BUILT_IN_FUNCTIONS, KEYWORDS_OPERATORS],
relevance: 10
},
];
// 直接的 SQL 语句规则 - 用于双大括号中的完整SQL
const DIRECT_SQL_STATEMENT = {
// 匹配以 SELECT 开头的语句
begin: /\b(SELECT)\b/i,
end: /(?=\}\}|$)/,
returnBegin: true,
className: 'string',
contains: SQL_CONTENT_RULES,
relevance: 20
};
// SQL 字符串规则 - 专门用于包含 SQL 语句的字符串
const SQL_STRING = {
className: 'string',
begin: /"/,
end: /"/,
contains: SQL_CONTENT_RULES,
relevance: 8
};
// 检测 SQL 查询函数后的字符串
const SQL_FUNCTION_CALL = {
begin: /\b(queryBlocks|querySpans|querySQL)\s+/,
end: /(?=\s+\$|\s*\)|\s*$)/,
returnBegin: true,
contains: [
// 函数名
{
className: 'built_in',
begin: /\b(queryBlocks|querySpans|querySQL)\b/,
relevance: 10
},
// 跟在函数名后的 SQL 字符串
SQL_STRING
],
relevance: 15
};
// 普通字符串规则
const STRING_RULE = {
className: 'string',
variants: [
{ begin: /"/, end: /"/ },
{ begin: /'/, end: /'/ }
],
relevance: 5
};
// 注释规则
const COMMENT_RULE = {
className: 'comment',
variants: [
{ begin: /\/\*/, end: /\*\// },
{ begin: /\/\//, end: /$/ }
],
relevance: 10
};
// .action 块规则
const ACTION_BLOCK = {
begin: /(\.action\{)/,
end: /(\})/,
beginScope: 'selector-pseudo',
endScope: 'selector-pseudo',
contains: [
COMMENT_RULE,
SQL_FUNCTION_CALL, // SQL 函数调用(优先级较高)
VARIABLE_RULE,
BUILT_IN_FUNCTIONS,
KEYWORDS_OPERATORS,
STRING_RULE, // 普通字符串(优先级较低)
// 数字
{
className: 'number',
begin: /\b\d+(\.\d+)?\b/,
relevance: 0
},
// 操作符
{
className: 'operator',
begin: /(:=|==|!=|<=|>=|<|>|=)/,
relevance: 5
},
...goRules.contains,
],
relevance: 10
};
// 双大括号块规则 - 增强 SQL 支持
const CURLY_BLOCK = {
begin: /(\{\{)/,
end: /(\}\})/,
beginScope: 'selector-pseudo',
endScope: 'selector-pseudo',
contains: [
COMMENT_RULE,
DIRECT_SQL_STATEMENT, // 直接的SQL语句最高优先级
VARIABLE_RULE,
BUILT_IN_FUNCTIONS,
KEYWORDS_OPERATORS,
STRING_RULE,
...goRules.contains,
],
relevance: 10
};
// 思源块属性规则
const BLOCK_ATTR_RULE = {
className: 'comment',
begin: /\{:/,
end: /\}(?=\s*$)/,
contains: [
{
className: 'string',
begin: /\"/,
end: /\"/,
contains: [
{
begin: /(\.action\{)/,
end: /(\})/,
beginScope: 'selector-pseudo',
endScope: 'selector-pseudo',
contains: [
VARIABLE_RULE,
BUILT_IN_FUNCTIONS,
KEYWORDS_OPERATORS,
STRING_RULE,
],
relevance: 10
},
]
},
{
className: 'title',
begin: /\b\w+=/,
}
],
relevance: 10
};
return {
name: 'template',
aliases: ['siyuan-template', 'template'],
case_insensitive: true,
contains: [
ACTION_BLOCK,
CURLY_BLOCK,
BLOCK_ATTR_RULE,
...markdownRules.contains,
]
};
});

View file

@ -206,6 +206,28 @@ func DocSaveAsTemplate(id, name string, overwrite bool) (code int, err error) {
return ast.WalkContinue return ast.WalkContinue
}) })
var unlinks []*ast.Node
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeCodeBlockFenceInfoMarker == n.Type {
if lang := string(n.CodeBlockInfo); "siyuan-template" == lang || "template" == lang {
// 将模板代码转换为段落文本 https://github.com/siyuan-note/siyuan/pull/15345
unlinks = append(unlinks, n.Parent)
p := treenode.NewParagraph(n.Parent.ID)
// 代码块内可能会有多个空行,但是这里不需要分块处理,后面渲染一个文本节点即可
p.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: n.Next.Tokens})
n.Parent.InsertBefore(p)
}
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
luteEngine := NewLute() luteEngine := NewLute()
formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions) formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
md := formatRenderer.Render() md := formatRenderer.Render()