🎨 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

@ -1458,7 +1458,7 @@
"22": "كلمة التحقق غير صحيحة",
"23": "مستودع البيانات تالف، الرجاء إعادة تعيينه",
"24": "انتهت مهلة الشبكة، يرجى المحاولة مرة أخرى في وقت لاحق",
"25": "اسم السمة يدعم فقط الأحرف الإنجليزية والأرقام",
"25": "يمكن أن يحتوي اسم السمة فقط على أحرف إنجليزية صغيرة وأرقام وشرطات، ويجب أن يبدأ بحرف إنجليزي صغير",
"26": "الرجاء تهيئة مفتاح مستودع البيانات أولاً في [الإعدادات - حول - مفتاح مستودع البيانات]",
"27": "‫جارٍ رفع [%v]",
"28": "الشبكة غير طبيعية، يرجى المحاولة مرة أخرى لاحقاً",

File diff suppressed because it is too large Load diff

View file

@ -1458,7 +1458,7 @@
"22": "The captcha is incorrect",
"23": "The data repo is corrupted, please reset the data repo",
"24": "Network timed out, please try again later",
"25": "The attribute name only supports English letters and digits",
"25": "The attribute name can only contain lowercase English letters, digits, and hyphens, and must start with a lowercase English letter",
"26": "Please initialize the data repo key first in [Settings - About - Data repo key]",
"27": "Uploading [%v]",
"28": "The network is abnormal, please try again later",

View file

@ -1458,7 +1458,7 @@
"22": "El captcha es incorrecto",
"23": "El repositorio de datos está dañado, reinicie el repositorio de datos",
"24": "Se agotó el tiempo de espera de la red. Vuelva a intentarlo más tarde.",
"25": "El nombre del atributo sólo admite letras y dígitos en inglés",
"25": "El nombre del atributo solo puede contener letras minúsculas, dígitos y guiones, y debe comenzar con una letra minúscula",
"26": "Por favor, inicialice primero la clave de repositorio de datos en [Configuración - Acerca de - Clave de repositorio de datos]",
"27": "Subiendo [%v]",
"28": "La red es anómala, inténtalo de nuevo más tarde",

View file

@ -1458,7 +1458,7 @@
"22": "Le captcha est incorrect",
"23": "Le référentiel de données est corrompu, veuillez réinitialiser le référentiel de données",
"24": "Le réseau a expiré, veuillez réessayer plus tard",
"25": "Le nom de l'attribut ne supporte que les lettres et les chiffres anglais.",
"25": "Le nom de l'attribut ne peut contenir que des lettres minuscules, des chiffres et des tirets, et doit commencer par une lettre minuscule",
"26": "Veuillez d'abord initialiser la clé du référentiel de données dans [Paramètres - À propos - Clé du référentiel de données]",
"27": "Téléchargement de [%v]",
"28": "Le réseau est anormal, veuillez réessayer plus tard",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1458,7 +1458,7 @@
"22": "キャプチャが正しくありません",
"23": "データリポジトリが壊れています。データリポジトリをリセットしてください",
"24": "ネットワークがタイムアウトしました。後でまた試してださい",
"25": "属性名は英字と数字のみ対応しています",
"25": "属性名は小文字の英字、数字、ハイフンのみを含むことができ、小文字の英字で始まる必要があります",
"26": "[設定] - [情報] - [データリポジトリキー] でデータリポジトリキーを初期化してください",
"27": "[%v] をアップロード中",
"28": "ネットワークに問題があります。後でまた試してださい",

View file

@ -1458,7 +1458,7 @@
"22": "보안 문자가 올바르지 않습니다",
"23": "데이터 저장소가 손상되었습니다. 데이터 저장소를 재설정하세요",
"24": "네트워크 시간 초과, 나중에 다시 시도하세요",
"25": "속성 이름은 영문자와 숫자만 지원합니다",
"25": "속성 이름은 소문자 영문자, 숫자, 하이픈만 포함할 수 있으며 소문자 영문자로 시작해야 합니다",
"26": "먼저 [설정 - 정보 - 데이터 저장소 키]에서 데이터 저장소 키를 초기화하세요",
"27": "업로드 중 [%v]",
"28": "네트워크가 비정상입니다. 나중에 다시 시도하세요",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1458,7 +1458,7 @@
"22": "Doğrulama kodu hatalı",
"23": "Veri deposu bozulmuş, lütfen veri deposunu sıfırla",
"24": "Ağ zaman aşımına uğradı, lütfen daha sonra tekrar dene",
"25": "Öznitelik adı yalnızca İngilizce harf ve rakam içerebilir",
"25": "Öznitelik adı yalnızca küçük harf İngilizce harf, rakam ve tire içerebilir ve küçük harf İngilizce harf ile başlamalıdır",
"26": "Lütfen önce [Ayarlar - Hakkında - Veri deposu anahtarı] kısmından veri deposu anahtarını başlat",
"27": "[%v] yükleniyor",
"28": "Ağ hatalı, lütfen daha sonra tekrar dene",

View file

@ -1458,7 +1458,7 @@
"22": "驗證碼不正確",
"23": "資料倉庫已被損壞,請重置資料倉庫",
"24": "網絡超時,請稍後再試",
"25": "屬性名僅支援英文字母和阿拉伯數字",
"25": "屬性名只能包含小寫英文字母、數字和連字符,並且以小寫英文字母開頭",
"26": "請先在 [設置 - 關於 - 資料倉庫密鑰] 中初始化資料倉庫密鑰",
"27": "正在上傳 [%v]",
"28": "網絡異常,請稍後再試",

View file

@ -1458,7 +1458,7 @@
"22": "验证码不正确",
"23": "数据仓库已被损坏,请重置数据仓库",
"24": "网络超时,请稍后再试",
"25": "属性名仅支持英文字母和阿拉伯数字",
"25": "属性名只能包含小写英文字母、数字和连字符,并且以小写英文字母开头",
"26": "请先在 [设置 - 关于 - 数据仓库密钥] 中初始化数据仓库密钥",
"27": "正在上传 [%v]",
"28": "网络异常,请稍后再试",

View file

@ -265,6 +265,15 @@
"Type": "NodeText",
"Data": "After sharing, write the attribute "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
"TextMarkTextContent": "custom-liandi-articleid"
},
{
"Type": "NodeText",
"Data": " (previously "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
@ -272,7 +281,7 @@
},
{
"Type": "NodeText",
"Data": " to the document to associate the link post ID"
"Data": " ) to the document to associate the link post ID"
}
]
}

View file

@ -265,6 +265,15 @@
"Type": "NodeText",
"Data": "分享完毕后对文档写入属性 "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
"TextMarkTextContent": "custom-liandi-articleid"
},
{
"Type": "NodeText",
"Data": "​(原为 "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
@ -272,7 +281,7 @@
},
{
"Type": "NodeText",
"Data": " 用于关联链滴帖子 ID"
"Data": " 用于关联链滴帖子 ID"
}
]
}

View file

@ -265,6 +265,15 @@
"Type": "NodeText",
"Data": "分享完畢後對文檔寫入屬性 "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
"TextMarkTextContent": "custom-liandi-articleid"
},
{
"Type": "NodeText",
"Data": "​(原為 "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
@ -272,7 +281,7 @@
},
{
"Type": "NodeText",
"Data": " 用於關聯鏈滴帖子 ID"
"Data": " 用於關聯鏈滴帖子 ID"
}
]
}

View file

@ -282,6 +282,15 @@
"Type": "NodeText",
"Data": "公開されると、ドキュメントに "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
"TextMarkTextContent": "custom-liandi-articleid"
},
{
"Type": "NodeText",
"Data": "​(以前は "
},
{
"Type": "NodeTextMark",
"TextMarkType": "code",
@ -289,7 +298,7 @@
},
{
"Type": "NodeText",
"Data": " 属性が書き込まれ、リンクされた投稿 ID を関連付けます"
"Data": " 属性が書き込まれ、リンクされた投稿 ID を関連付けます"
}
]
}

View file

@ -363,7 +363,7 @@ export class Wnd {
if (!oldTab) { // 从主窗口拖拽到页签新窗口
JSONToCenter(app, tabData, this);
this.children.find(item => {
if (item.headElement.getAttribute("data-activeTime") === tabData.activeTime) {
if (item.headElement.getAttribute("data-activetime") === tabData.activeTime) {
oldTab = item;
return true;
}

View file

@ -869,7 +869,7 @@ export class Dock {
if (typeof tabIndex === "undefined" && !TYPES.includes(item.type)) {
return;
}
html += `<span data-height="${item.size.height}" data-width="${item.size.width}" data-type="${item.type}" data-index="${index}" data-hotkey="${item.hotkey || ""}" data-hotkeyLangId="${item.hotkeyLangId || ""}" data-title="${item.title}" class="dock__item${item.show ? " dock__item--active" : ""} ariaLabel" aria-label="<span style='white-space:pre'>${item.title} ${item.hotkey ? updateHotkeyTip(item.hotkey) : ""}${window.siyuan.languages.dockTip}</span>">
html += `<span data-height="${item.size.height}" data-width="${item.size.width}" data-type="${item.type}" data-index="${index}" data-hotkey="${item.hotkey || ""}" data-hotkeylangid="${item.hotkeyLangId || ""}" data-title="${item.title}" class="dock__item${item.show ? " dock__item--active" : ""} ariaLabel" aria-label="<span style='white-space:pre'>${item.title} ${item.hotkey ? updateHotkeyTip(item.hotkey) : ""}${window.siyuan.languages.dockTip}</span>">
<svg><use xlink:href="#${item.icon}"></use></svg>
</span>`;
this.data[item.type] = true;

View file

@ -143,7 +143,7 @@ const dockToJSON = (dock: Dock) => {
show: item.classList.contains("dock__item--active"),
icon: item.querySelector("use").getAttribute("xlink:href").substring(1),
hotkey: item.getAttribute("data-hotkey") || "",
hotkeyLangId: item.getAttribute("data-hotkeyLangId") || ""
hotkeyLangId: item.getAttribute("data-hotkeylangid") || ""
});
});
return data;

View file

@ -2,7 +2,7 @@
import {shell} from "electron";
/// #endif
import {confirmDialog} from "../dialog/confirmDialog";
import {getSearch, isMobile, isValidAttrName} from "../util/functions";
import {getSearch, isMobile, isValidCustomAttrName} from "../util/functions";
import {isLocalPath, movePathTo, moveToPath, pathPosix} from "../util/pathName";
import {MenuItem} from "./Menu";
import {onExport, saveExport} from "../protyle/export";
@ -353,8 +353,8 @@ export const openFileAttr = (attrs: IObject, focusName = "bookmark", protyle?: I
});
btnsElement[1].addEventListener("click", () => {
const value = inputElement.value.toLowerCase();
if (!isValidAttrName(value)) {
showMessage(window.siyuan.languages.attrName + " <b>" + escapeHtml(value) + "</b> " + window.siyuan.languages.invalid);
if (!isValidCustomAttrName(value)) {
showMessage(window.siyuan.languages._kernel[25]);
return false;
}
let existElement: HTMLElement | false;

View file

@ -83,8 +83,8 @@ export const isFileAnnotation = (text: string) => {
return /^<<assets\/.+\/\d{14}-\w{7} ".+">>$/.test(text);
};
export const isValidAttrName = (name: string) => {
return /^[_a-zA-Z][_.\-0-9a-zA-Z]*$/.test(name);
export const isValidCustomAttrName = (name: string) => {
return /^[a-z][\-0-9a-z]*$/.test(name);
};
// REF https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval

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 {