feat(ui): redesign public reading and complete glass workspace

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.
This commit is contained in:
eezd committed 2026-10-22 15:43:52 +08:00
1 parent 91a359c2b7
commit 4d1339851b
28 files changed
+472 -66

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
package vault
import (
"bytes"
"unicode"
"unicode/utf8"
"github.com/yuin/goldmark/v2/ast"
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
"github.com/yuin/goldmark/v2/text"
"github.com/yuin/goldmark/v2/util"
)
// 仅供关系提取:公式与代码同属非正文,不能把其中的 #1 或示例链接当成知识关系。
// 分隔规则与前端 Typora 扩展一致;此解析器不参与 Markdown 渲染。
type metadataMath struct{}
func (metadataMath) Trigger() []byte { return []byte{'$'} }
func (metadataMath) Open(parent ast.Node, reader text.Reader, _ parser.Context) (ast.Node, parser.State) {
line, _ := reader.PeekLine()
if parent.Kind() != ast.KindDocument || !bytes.Equal(bytes.TrimSpace(line), []byte("$$")) {
return nil, parser.NoChildren
}
return ast.NewCodeBlock(ast.CodeBlockKindFenced, text.Lines{}), parser.NoChildren
}
func (metadataMath) Continue(node ast.Node, reader text.Reader, _ parser.Context) parser.State {
line, segment := reader.PeekLine()
reader.AdvanceToEOL()
if bytes.Equal(bytes.TrimSpace(line), []byte("$$")) {
return parser.Close
}
node.(*ast.CodeBlock).Value.AppendSegment(segment)
return parser.Continue | parser.NoChildren
}
func (metadataMath) Close(ast.Node, text.Reader, parser.Context) {}
func (metadataMath) CanInterruptParagraph() bool { return true }
func (metadataMath) CanAcceptIndentedLine() bool { return false }
func (metadataMath) Parse(_ ast.Node, reader text.Reader, _ parser.Context) ast.Node {
line, first := reader.PeekLine()
if len(line) < 2 || line[1] == '$' {
return nil
}
next, _ := utf8.DecodeRune(line[1:])
if unicode.IsSpace(next) {
return nil
}
savedLine, savedPosition := reader.Position()
reader.Advance(1)
for {
line, segment := reader.PeekLine()
if line == nil {
reader.SetPosition(savedLine, savedPosition)
return nil
}
for i := 0; i < len(line); i++ {
if line[i] == '\\' {
i++
continue
}
if line[i] != '$' || i == 0 {
continue
}
prev, _ := utf8.DecodeLastRune(line[:i])
if unicode.IsSpace(prev) {
continue
}
if i+1 < len(line) && line[i+1] >= '0' && line[i+1] <= '9' {
continue
}
reader.Advance(i + 1)
return ast.NewCodeSpan(text.NewSingleLineValueFromIndex(text.NewIndex(first.Start, segment.Start+i+1), nil))
}
reader.AdvanceLine()
}
}
func parseMetadata(source []byte) ast.Node {
math := metadataMath{}
return parser.New(parser.WithExtensions(extension.GFMParser, extension.FootnoteParser), parser.WithBlockParsers(util.Prioritized[parser.BlockParser](math, 650)), parser.WithInlineParsers(util.Prioritized[parser.InlineParser](math, 150))).Parse(source)
}
// 返回非正文的字节范围,供无损链接改写器跳过公式中的示例语法。
func nonProseRanges(root ast.Node) [][2]int {
var ranges [][2]int
_ = ast.Walk(root, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
switch node := node.(type) {
case *ast.CodeSpan:
for _, index := range node.Value.Indices() {
ranges = append(ranges, [2]int{index.Start, index.Stop})
}
return ast.WalkSkipChildren, nil
case *ast.CodeBlock:
for _, index := range node.Value.Segments() {
ranges = append(ranges, [2]int{index.Start, index.Stop})
}
return ast.WalkSkipChildren, nil
}
return ast.WalkContinue, nil
})
return ranges
}