🎨 Element attribute names are uniformly lowercase English letters https://github.com/siyuan-note/siyuan/issues/16604 (#16657)

部分属性名大写字母改为小写

兼容旧版带大写字母的属性名

更新用户指南说明

优化性能

统一前后端验证属性名的逻辑

改进验证属性名格式报错信息
This commit is contained in:
Jeffrey Chen 2025-12-22 09:43:12 +08:00 committed by GitHub
parent e1f6b83d35
commit 2d1618e639
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 10420 additions and 10359 deletions

View file

@ -19,13 +19,13 @@ package model
import (
"errors"
"fmt"
"maps"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/lex"
"github.com/88250/lute/parse"
"github.com/araddon/dateparse"
"github.com/siyuan-note/siyuan/kernel/cache"
@ -35,6 +35,32 @@ import (
"github.com/siyuan-note/siyuan/kernel/util"
)
// isValidAttrName 验证属性名是否合法
func isValidAttrName(name string) bool {
if len(name) == 0 {
return false
}
// 首字符必须是小写字母
if name[0] < 'a' || name[0] > 'z' {
return false
}
// 自定义属性 custom- 之后的首个字符必须是小写字母
if strings.HasPrefix(name, "custom-") {
if len(name) <= 7 || name[7] < 'a' || name[7] > 'z' {
return false
}
}
// 后续字符只能是小写字母、数字、连字符
for i := 1; i < len(name); i++ {
c := name[i]
if c == '-' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
continue
}
return false
}
return true
}
func SetBlockReminder(id string, timed string) (err error) {
if !IsSubscriber() {
if "ios" == util.Container {
@ -205,68 +231,64 @@ func setNodeAttrsWithTx(tx *Transaction, node *ast.Node, tree *parse.Tree, nameV
func setNodeAttrs0(node *ast.Node, nameValues map[string]string) (oldAttrs map[string]string, err error) {
oldAttrs = parse.IAL2Map(node.KramdownIAL)
for name := range nameValues {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
err = errors.New(fmt.Sprintf(Conf.Language(25), node.ID))
return
}
}
}
if tag, ok := nameValues["tags"]; ok {
var tags []string
tmp := strings.Split(tag, ",")
for _, t := range tmp {
t = util.RemoveInvalid(t)
t = strings.TrimSpace(t)
if "" != t {
tags = append(tags, t)
}
}
tags = gulu.Str.RemoveDuplicatedElem(tags)
if 0 < len(tags) {
nameValues["tags"] = strings.Join(tags, ",")
}
}
normalizeKeysToLower(nameValues)
newAttrs := maps.Clone(oldAttrs)
for name, value := range nameValues {
value = util.RemoveInvalidRetainCtrl(value)
value = strings.TrimSpace(value)
value = strings.TrimSuffix(value, ",")
lowerName := strings.ToLower(name)
// 转换为小写再验证属性名
if !isValidAttrName(lowerName) {
err = errors.New(Conf.Language(25) + " [" + node.ID + "]")
return
}
// 处理文档标签 https://github.com/siyuan-note/siyuan/issues/13311
if lowerName == "tags" {
var tags []string
tmp := strings.Split(value, ",")
for _, t := range tmp {
t = util.RemoveInvalid(t)
t = strings.TrimSpace(t)
if "" != t {
tags = append(tags, t)
}
}
tags = gulu.Str.RemoveDuplicatedElem(tags)
if 0 < len(tags) {
value = strings.Join(tags, ",")
} else {
value = ""
}
}
if "" == value {
node.RemoveIALAttr(name)
// 删除属性
if name != lowerName {
if _, exists := newAttrs[name]; exists {
// 仅删除完全匹配的包含大写字母的属性
delete(newAttrs, name)
continue
}
}
delete(newAttrs, lowerName)
} else {
node.SetIALAttr(name, value)
// 添加或更新属性
// 删除大小写完全匹配的属性
delete(newAttrs, name)
// 保存小写的属性 https://github.com/siyuan-note/siyuan/issues/16447
newAttrs[lowerName] = value
}
}
if oldAttrs["tags"] != nameValues["tags"] {
node.KramdownIAL = parse.Map2IAL(newAttrs)
if oldAttrs["tags"] != newAttrs["tags"] {
ReloadTag()
}
return
}
// normalizeKeysToLower 将 nameValues 的键统一为小写 https://github.com/siyuan-note/siyuan/issues/16447
func normalizeKeysToLower(nameValues map[string]string) {
newMap := make(map[string]string, len(nameValues))
for name, value := range nameValues {
lower := strings.ToLower(name)
newMap[lower] = value
}
for k := range nameValues {
delete(nameValues, k)
}
for k, v := range newMap {
nameValues[k] = v
}
}
func pushBroadcastAttrTransactions(oldAttrs map[string]string, node *ast.Node) {
newAttrs := parse.IAL2Map(node.KramdownIAL)
data := map[string]interface{}{"old": oldAttrs, "new": newAttrs}
@ -294,15 +316,15 @@ func ResetBlockAttrs(id string, nameValues map[string]string) (err error) {
}
for name := range nameValues {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
return errors.New(fmt.Sprintf(Conf.Language(25), id))
}
if !isValidAttrName(name) {
return errors.New(Conf.Language(25) + " [" + id + "]")
}
}
node.ClearIALAttrs()
for name, value := range nameValues {
value = util.RemoveInvalidRetainCtrl(value)
value = strings.TrimSpace(value)
if "" != value {
node.SetIALAttr(name, value)
}

View file

@ -245,9 +245,14 @@ func Export2Liandi(id string) (err error) {
defer util.PushClearMsg(msgId)
// 判断帖子是否已经存在,存在则使用更新接口
const liandiArticleIdAttrName = "custom-liandi-articleId"
const liandiArticleIdAttrName = "custom-liandi-articleid"
const liandiArticleIdAttrNameOld = "custom-liandi-articleId" // 兼容旧属性名
foundArticle := false
// 优先使用新属性名,如果不存在则尝试旧属性名
articleId := tree.Root.IALAttr(liandiArticleIdAttrName)
if "" == articleId {
articleId = tree.Root.IALAttr(liandiArticleIdAttrNameOld)
}
if "" != articleId {
result := gulu.Ret.NewResult()
request := httpclient.NewCloudRequest30s()

View file

@ -31,7 +31,6 @@ import (
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/lex"
"github.com/88250/lute/parse"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
@ -1736,11 +1735,9 @@ func (tx *Transaction) doSetAttrs(operation *Operation) (ret *TxErr) {
var invalidNames []string
for name := range attrs {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
logging.LogWarnf("invalid attr name [%s]", name)
invalidNames = append(invalidNames, name)
}
if !isValidAttrName(name) {
logging.LogWarnf("invalid attr name [%s]", name)
invalidNames = append(invalidNames, name)
}
}
for _, name := range invalidNames {
@ -1748,6 +1745,7 @@ func (tx *Transaction) doSetAttrs(operation *Operation) (ret *TxErr) {
}
for name, value := range attrs {
name := strings.ToLower(name)
if "" == value {
node.RemoveIALAttr(name)
} else {