🎨 Add "Remove ID from asset name" switch to export settings https://github.com/siyuan-note/siyuan/issues/16065

Signed-off-by: Daniel <845765@qq.com>
This commit is contained in:
Daniel 2026-01-16 21:59:21 +08:00
parent 11115da3d0
commit 066a9b5192
No known key found for this signature in database
GPG key ID: 86211BA83DF03017
21 changed files with 128 additions and 11 deletions

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "إزالة الـ ID من أسماء ملفات الموارد",
"removeAssetsIDTip": "عند التفعيل سيتم إزالة جزء الـ ID من أسماء ملفات الموارد عند تصدير Markdown",
"clearTempFiles": "Temporäre Dateien bereinigen",
"clearTempFilesTip": "Bereinigen Sie temporäre Dateien, die während der Ausführung der Anwendung erstellt wurden, um Speicherplatz freizugeben",
"uploadFileTooLarge": "⚠️ الملف الذي تم رفعه [${x}] كبير جدًا [${y}],هل تؤكد المتابعة بالرفع?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "ID aus Asset-Dateinamen entfernen",
"removeAssetsIDTip": "Wenn aktiviert, wird beim Export nach Markdown der ID-Teil aus den Asset-Dateinamen entfernt",
"clearTempFiles": "Temporäre Dateien bereinigen",
"clearTempFilesTip": "Bereinigen Sie temporäre Dateien, die während der Ausführung der Anwendung erstellt wurden, um Speicherplatz freizugeben",
"uploadFileTooLarge": "⚠️ Die hochgeladene Datei [${x}] ist zu groß [${y}]. Sind Sie sicher, dass Sie den Upload fortsetzen möchten?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Remove ID from asset file names",
"removeAssetsIDTip": "When enabled, the ID part will be removed from asset file names when exporting Markdown",
"clearTempFiles": "Clear temporary files",
"clearTempFilesTip": "Clear temporary files generated while the app is running to free up storage space",
"uploadFileTooLarge": "⚠️ The uploaded file [${x}] is too large [${y}]. Are you sure you want to continue uploading?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Eliminar el ID de los nombres de archivo de los recursos",
"removeAssetsIDTip": "Al habilitar, al exportar a Markdown se eliminará la parte del nombre de archivo de recursos que contiene el ID",
"clearTempFiles": "Limpiar archivos temporales",
"clearTempFilesTip": "Limpia los archivos temporales generados durante la ejecución de la aplicación para liberar espacio de almacenamiento",
"uploadFileTooLarge": "⚠️ El archivo subido [${x}] es demasiado grande [${y}]¿estás seguro de que deseas continuar con la subida?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Supprimer l'ID des noms de fichiers des ressources",
"removeAssetsIDTip": "Une fois activé, lors de l'export en Markdown, la partie ID dans le nom des fichiers de ressources sera supprimée",
"clearTempFiles": "Effacer les fichiers temporaires",
"clearTempFilesTip": "Effacez les fichiers temporaires générés lors de l'exécution de l'application pour libérer de l'espace de stockage",
"uploadFileTooLarge": "⚠️ Le fichier envoyé [${x}] est trop volumineux [${y}]. Voulez-vous continuer l'envoi ?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "הסר את הID משמות קבצי המשאבים",
"removeAssetsIDTip": "כאשר מופעל, בעת ייצוא לMarkdown יוסר חלק הID משמות קבצי המשאבים",
"clearTempFiles": "ניקוי קבצים זמניים",
"clearTempFilesTip": "נקה קבצים זמניים שנוצרו במהלך הפעלת היישום כדי לפנות מקום אחסון",
"uploadFileTooLarge": "⚠️ הקובץ שהועלה [${x}] גדול מאוד [${y}],אשר האם להמשיך בהעלאה?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Rimuovi l'ID dai nomi dei file delle risorse",
"removeAssetsIDTip": "Se abilitato, durante l'esportazione in Markdown verrà rimossa la parte ID dai nomi dei file delle risorse",
"clearTempFiles": "Elimina file temporanei",
"clearTempFilesTip": "Elimina i file temporanei creati durante l'esecuzione dell'app per liberare spazio di archiviazione",
"uploadFileTooLarge": "⚠️ Il file caricato [${x}] è troppo grande [${y}]. Sei sicuro di voler continuare il caricamento?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "リソースファイル名から ID を削除",
"removeAssetsIDTip": "有効にすると Markdown をエクスポートする際にリソースファイル名の ID 部分を削除します",
"clearTempFiles": "一時ファイルを削除",
"clearTempFilesTip": "アプリの実行中に生成された一時ファイルを削除して、ストレージ領域を解放します",
"uploadFileTooLarge": "⚠️ アップロードしたファイル [${x}] は [${y}] で大きすぎます。アップロードを続行しますか?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "리소스 파일 이름의 ID 제거",
"removeAssetsIDTip": "활성화하면 Markdown 내보내기 시 리소스 파일 이름의 ID 부분을 제거합니다",
"clearTempFiles": "임시 파일 정리",
"clearTempFilesTip": "앱 실행 중 생성된 임시 파일을 정리하여 저장 공간을 확보합니다",
"uploadFileTooLarge": "⚠️ 업로드한 파일 [${x}]의 크기가 [${y}]로 너무 큽니다. 계속 업로드하시겠습니까?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Usuń ID z nazw plików zasobów",
"removeAssetsIDTip": "Po włączeniu podczas eksportu do Markdown zostanie usunięta część nazwy pliku zasobu zawierająca ID",
"clearTempFiles": "Wyczyść pliki tymczasowe",
"clearTempFilesTip": "Wyczyść pliki tymczasowe tworzone podczas działania aplikacji, aby zwolnić miejsce na dysku",
"uploadFileTooLarge": "⚠️ Przesłany plik [${x}] jest za duży [${y}]. Czy na pewno chcesz kontynuować przesyłanie?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Remover o ID dos nomes dos arquivos de recursos",
"removeAssetsIDTip": "Ao ativar, ao exportar para Markdown a parte do nome do arquivo de recursos contendo o ID será removida",
"clearTempFiles": "Limpar arquivos temporários",
"clearTempFilesTip": "Limpe os arquivos temporários gerados durante a execução do aplicativo para liberar espaço de armazenamento",
"uploadFileTooLarge": "⚠️ O arquivo enviado [${x}] é muito grande [${y}]。 Tem certeza de que deseja continuar o upload?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Удалить ID из имени файла ресурса",
"removeAssetsIDTip": "При включении при экспорте в Markdown из имён файлов ресурсов будет удалена часть с ID",
"clearTempFiles": "Очистить временные файлы",
"clearTempFilesTip": "Очистите временные файлы, созданные при работе приложения, чтобы освободить место в хранилище",
"uploadFileTooLarge": "⚠️ Загруженный файл [${x}] очень большой [${y}]. Вы уверены, что хотите продолжить загрузку?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "Varlık dosya adlarındaki ID'yi kaldır",
"removeAssetsIDTip": "Etkinleştirildiğinde Markdown dışa aktarılırken varlık dosya adlarındaki ID kısmı kaldırılacaktır",
"clearTempFiles": "Geçici dosyaları temizle",
"clearTempFilesTip": "Uygulama çalışırken oluşan geçici dosyaları temizleyerek depolama alanını boşalt",
"uploadFileTooLarge": "⚠️ Yüklenen dosya [${x}], boyutu [${y}] kadar büyük. Yüklemeye devam etmek istediğinize emin misiniz?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "移除資源檔名中的 ID",
"removeAssetsIDTip": "啟用後在匯出 Markdown 時會移除資源檔名中的 ID 部分",
"clearTempFiles": "清理臨時檔案",
"clearTempFilesTip": "清理應用運行過程中產生的臨時檔案以釋放儲存空間",
"uploadFileTooLarge": "⚠️ 上傳的檔案 [${x}] 很大 [${y}],請確認是否繼續上傳?",

View file

@ -1,4 +1,6 @@
{
"removeAssetsID": "移除资源文件名中的 ID",
"removeAssetsIDTip": "启用后在导出 Markdown 时将移除资源文件名中的 ID 部分",
"clearTempFiles": "清理临时文件",
"clearTempFilesTip": "清理应用运行过程中产生的临时文件以释放存储空间",
"uploadFileTooLarge": "⚠️ 上传的文件 [${x}] 很大 [${y}],请确认是否继续上传?",

View file

@ -39,6 +39,14 @@ export const exportConfig = {
<span class="fn__space"></span>
<input class="b3-switch fn__flex-center" id="markdownYFM" type="checkbox"${window.siyuan.config.export.markdownYFM ? " checked" : ""}/>
</label>
<label class="fn__flex b3-label">
<div class="fn__flex-1">
${window.siyuan.languages.removeAssetsID}
<div class="b3-label__text">${window.siyuan.languages.removeAssetsIDTip}</div>
</div>
<span class="fn__space"></span>
<input class="b3-switch fn__flex-center" id="removeAssetsID" type="checkbox"${window.siyuan.config.export.removeAssetsID ? " checked" : ""}/>
</label>
<label class="fn__flex b3-label">
<div class="fn__flex-1">
${window.siyuan.languages.export31}
@ -207,6 +215,7 @@ export const exportConfig = {
fetchPost("/api/setting/setExport", {
paragraphBeginningSpace: (exportConfig.element.querySelector("#paragraphBeginningSpace") as HTMLInputElement).checked,
addTitle: (exportConfig.element.querySelector("#addTitle") as HTMLInputElement).checked,
removeAssetsID: (exportConfig.element.querySelector("#removeAssetsID") as HTMLInputElement).checked,
markdownYFM: (exportConfig.element.querySelector("#markdownYFM") as HTMLInputElement).checked,
inlineMemo: (exportConfig.element.querySelector("#inlineMemo") as HTMLInputElement).checked,
blockRefMode: parseInt((exportConfig.element.querySelector("#blockRefMode") as HTMLSelectElement).value, 10),

View file

@ -56,7 +56,7 @@ export const initConfigSearch = (element: HTMLElement, app: App) => {
// 导出
getLang(["paragraphBeginningSpace", "md4", "export", "export1", "export2", "export5", "export11",
"export13", "export14", "export15", "export19", "export20", "ref", "blockEmbed", "export17", "export18",
"export23", "export24", "export25", "export26", "export27", "export28", "export29"]),
"export23", "export24", "export25", "export26", "export27", "export28", "export29", "removeAssetsID", "removeAssetsIDTip"]),
// 外观
getLang(["language", "language1", "appearance", "appearance1", "appearance2", "appearance3", "appearance4",

View file

@ -574,6 +574,10 @@ declare namespace Config {
* Whether to add YAML Front Matter when exporting to Markdown
*/
markdownYFM: boolean;
/**
* Whether to remove the asset ID when exporting to Markdown
*/
removeAssetsID: boolean;
/**
* Whether to export the inline memo
*/

View file

@ -33,6 +33,7 @@ type Export struct {
TagCloseMarker string `json:"tagCloseMarker"` // 标签结束标记符,默认是 #
FileAnnotationRefMode int `json:"fileAnnotationRefMode"` // 文件标注引用导出模式0文件名 - 页码 - 锚文本1仅锚文本
PandocBin string `json:"pandocBin"` // Pandoc 可执行文件路径
RemoveAssetsID bool `json:"removeAssetsID"` // Markdown 导出时是否移除资源文件名 ID 部分 https://github.com/siyuan-note/siyuan/issues/16065
MarkdownYFM bool `json:"markdownYFM"` // Markdown 导出时是否添加 YAML Front Matter https://github.com/siyuan-note/siyuan/issues/7727
InlineMemo bool `json:"inlineMemo"` // 是否导出行级备注 https://github.com/siyuan-note/siyuan/issues/14605
PDFFooter string `json:"pdfFooter"` // PDF 导出时页脚内容
@ -55,6 +56,7 @@ func NewExport() *Export {
TagCloseMarker: "#",
FileAnnotationRefMode: 0,
PandocBin: "",
RemoveAssetsID: false,
MarkdownYFM: false,
InlineMemo: false,
PDFFooter: "%page / %pages",

View file

@ -1353,6 +1353,23 @@ func getAssetsLinkDests(node *ast.Node, includeServePath bool) (ret []string) {
return
}
func getAssetsLinkDestsInTree(tree *parse.Tree, includeServePath bool) (nodes []*ast.Node) {
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
dests := getAssetsLinkDests(n, includeServePath)
if 1 > len(dests) {
return ast.WalkContinue
}
nodes = append(nodes, n)
return ast.WalkContinue
})
return
}
func setAssetsLinkDest(node *ast.Node, oldDest, dest string) {
if ast.NodeLinkDest == node.Type {
if bytes.HasPrefix(node.Tokens, []byte("//")) {

View file

@ -1449,7 +1449,9 @@ func processPDFLinkEmbedAssets(pdfCtx *model.Context, assetDests []string, remov
now := types.StringLiteral(types.DateString(time.Now()))
for _, link := range assetLinks {
link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":"+util.ServerPort+"/export/temp/", "")
link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":6806/export/temp/", "")
link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":"+util.ServerPort+"/", "") // Exporting PDF embedded asset files as attachments fails https://github.com/siyuan-note/siyuan/issues/7414#issuecomment-1704573557
link.URI = strings.ReplaceAll(link.URI, "http://"+util.LocalHost+":6806/", "")
link.URI, _ = url.PathUnescape(link.URI)
if idx := strings.Index(link.URI, "?"); 0 < idx {
link.URI = link.URI[:idx]
@ -3324,6 +3326,7 @@ func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []strin
return
}
assetsOldNew, assetsNewOld := map[string]string{}, map[string]string{}
luteEngine := util.NewLute()
for i, p := range docPaths {
id := util.GetTreeID(p)
@ -3361,31 +3364,42 @@ func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []strin
// 解析导出后的标准 Markdown汇总 assets
tree = parse.Parse("", gulu.Str.ToBytes(md), luteEngine.ParseOptions)
var assets []string
assets = append(assets, getAssetsLinkDests(tree.Root, false)...)
for _, asset := range assets {
asset = string(html.DecodeDestination([]byte(asset)))
if strings.Contains(asset, "?") {
asset = asset[:strings.LastIndex(asset, "?")]
removeAssetsID(tree, assetsOldNew, assetsNewOld)
newAssets := getAssetsLinkDests(tree.Root, false)
for _, newAsset := range newAssets {
newAsset = string(html.DecodeDestination([]byte(newAsset)))
if strings.Contains(newAsset, "?") {
newAsset = newAsset[:strings.LastIndex(newAsset, "?")]
}
if !strings.HasPrefix(asset, "assets/") {
if !strings.HasPrefix(newAsset, "assets/") {
continue
}
srcPath := assetsPathMap[asset]
oldAsset := assetsNewOld[newAsset]
if "" == oldAsset {
logging.LogWarnf("get asset old path for new asset [%s] failed", newAsset)
continue
}
srcPath := assetsPathMap[oldAsset]
if "" == srcPath {
logging.LogWarnf("get asset [%s] abs path failed", asset)
logging.LogWarnf("get asset [%s] abs path failed", oldAsset)
continue
}
destPath := filepath.Join(writeFolder, asset)
destPath := filepath.Join(writeFolder, newAsset)
if copyErr := filelock.Copy(srcPath, destPath); copyErr != nil {
logging.LogErrorf("copy asset from [%s] to [%s] failed: %s", srcPath, destPath, err)
continue
}
}
for assetsOld, assetsNew := range assetsOldNew {
md = strings.ReplaceAll(md, assetsOld, assetsNew)
}
// 调用 Pandoc 进行格式转换
pandocErr := util.Pandoc(pandocFrom, pandocTo, writePath, md)
if pandocErr != nil {
@ -3437,6 +3451,47 @@ func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []strin
return
}
func removeAssetsID(tree *parse.Tree, assetsOldNew, assetsNewOld map[string]string) {
assetNodes := getAssetsLinkDestsInTree(tree, false)
for _, node := range assetNodes {
dests := getAssetsLinkDests(node, false)
if 1 > len(dests) {
continue
}
for _, dest := range dests {
if !Conf.Export.RemoveAssetsID {
assetsOldNew[dest] = dest
assetsNewOld[dest] = dest
continue
}
if newDest := assetsOldNew[dest]; "" != newDest {
setAssetsLinkDest(node, dest, newDest)
continue
}
name := path.Base(dest)
name = util.RemoveID(name)
newDest := "assets/" + name
if existOld := assetsNewOld[newDest]; "" != existOld {
if existOld == dest { // 已存在相同资源路径
setAssetsLinkDest(node, dest, newDest)
} else {
// 存在同名但内容不同的资源文件,保留 ID
assetsNewOld[dest] = dest
assetsOldNew[dest] = dest
}
continue
}
setAssetsLinkDest(node, dest, newDest)
assetsOldNew[dest] = newDest
assetsNewOld[newDest] = dest
}
}
}
func getExportBlockRefLinkText(blockRef *ast.Node, blockRefTextLeft, blockRefTextRight string) (defID, linkText string) {
defID, linkText, _ = treenode.GetBlockRef(blockRef)
if "" == linkText {