Add scoped public navigation, reading progress, focus and print controls without loading the editor. Polish responsive settings, graph and media layouts. Correct share expiry units, preserve math and code during link maintenance, and normalize Markdown destinations for templates and note navigation. Verify browser flows and document the new tools.
144 lines
3.5 KiB
Go
144 lines
3.5 KiB
Go
package vault
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// ReplaceImage 原位替换已有图片,保留所有引用路径;不允许借此创建文件或改变格式。
|
|
func (v *Vault) ReplaceImage(rel, filename string, r io.Reader, limit int64) error {
|
|
if !IsImage(rel) || !strings.EqualFold(path.Ext(rel), path.Ext(filename)) {
|
|
return fmt.Errorf("%w:请选择与原图片扩展名相同的文件", ErrInvalidPath)
|
|
}
|
|
p, err := v.abs(rel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(r, limit+1))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if int64(len(data)) > limit {
|
|
return ErrTooLarge
|
|
}
|
|
if len(data) == 0 {
|
|
return fmt.Errorf("%w:图片不能为空", ErrInvalidPath)
|
|
}
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
info, err := os.Lstat(p)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return ErrInvalidPath
|
|
}
|
|
return atomicWrite(p, data)
|
|
}
|
|
|
|
type linkChange struct {
|
|
path string
|
|
before, after []byte
|
|
}
|
|
|
|
// 调用方持有写锁;先计算全部改写,移动成功后再写入,出错时保留回滚依据。
|
|
func (v *Vault) moveLinkChanges(from, to string, directory bool, ownAssets bool) ([]linkChange, error) {
|
|
entries, err := v.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var changes []linkChange
|
|
for _, entry := range entries {
|
|
if entry.Dir || !IsNote(entry.Path) {
|
|
continue
|
|
}
|
|
before, _, err := v.Read(entry.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
next := movedPath(entry.Path, from, to, directory)
|
|
after, err := rewriteMovedLinks(before, entry.Path, next, from, to, directory, ownAssets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ownAssets && entry.Path == from {
|
|
after = RewriteAssetLinks(after, strings.TrimSuffix(path.Base(from), NoteExt), strings.TrimSuffix(path.Base(to), NoteExt))
|
|
}
|
|
if !bytes.Equal(before, after) {
|
|
changes = append(changes, linkChange{filepath.Join(v.root, filepath.FromSlash(next)), before, after})
|
|
}
|
|
}
|
|
return changes, nil
|
|
}
|
|
|
|
func movedPath(p, from, to string, directory bool) string {
|
|
if p == from {
|
|
return to
|
|
}
|
|
if directory && strings.HasPrefix(p, from+"/") {
|
|
return to + strings.TrimPrefix(p, from)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func rewriteMovedLinks(content []byte, oldNote, newNote, from, to string, directory, ownAssets bool) ([]byte, error) {
|
|
var out []byte
|
|
last := 0
|
|
excluded := nonProseRanges(parseMetadata(content))
|
|
for _, span := range importLinkSpans(content) {
|
|
skip := false
|
|
for _, region := range excluded {
|
|
if span.start >= region[0] && span.start < region[1] {
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
if skip {
|
|
continue
|
|
}
|
|
if span.start < last {
|
|
continue
|
|
}
|
|
target, u, ok := LocalTarget(oldNote, string(content[span.start:span.end]))
|
|
if !ok {
|
|
continue
|
|
}
|
|
mapped := movedPath(target, from, to, directory)
|
|
if ownAssets {
|
|
mapped = movedPath(mapped, AssetsDir(from), AssetsDir(to), true)
|
|
}
|
|
if mapped == target && path.Dir(oldNote) == path.Dir(newNote) {
|
|
continue
|
|
}
|
|
rel, err := filepath.Rel(filepath.FromSlash(path.Dir(newNote)), filepath.FromSlash(mapped))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replacement := EscapeLinkPath(filepath.ToSlash(rel))
|
|
if u.RawQuery != "" {
|
|
replacement += "?" + u.RawQuery
|
|
}
|
|
if u.Fragment != "" {
|
|
replacement += "#" + u.EscapedFragment()
|
|
}
|
|
if span.html {
|
|
replacement = html.EscapeString(replacement)
|
|
}
|
|
out = append(out, content[last:span.start]...)
|
|
out = append(out, replacement...)
|
|
last = span.end
|
|
}
|
|
return append(out, content[last:]...), nil
|
|
}
|