feat: update gomark dependency and refactor markdown parsing logic

This commit is contained in:
Steven 2025-09-17 21:09:30 +08:00
parent b7f792cbf7
commit 5ad2038b1a
9 changed files with 76 additions and 53 deletions

2
go.mod
View file

@ -23,7 +23,7 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218
golang.org/x/crypto v0.41.0
golang.org/x/mod v0.27.0
golang.org/x/net v0.43.0

4
go.sum
View file

@ -433,8 +433,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4 h1:WUVmhqDHt+5nhHGnsdfZ8no8zdwhKLPQ5AT/IP57egI=
github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4/go.mod h1:7CZRoYFQyyljzplOTeyODFR26O+wr0BbnpTWVLGfKJA=
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218 h1:rkH9CKI0AzWOIkwzZOs0PYepWt9fJTZZ9c5W1lL/bMQ=
github.com/usememos/gomark v0.0.0-20250917125604-82623ecaf218/go.mod h1:7CZRoYFQyyljzplOTeyODFR26O+wr0BbnpTWVLGfKJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=

View file

@ -1,4 +1,4 @@
package util
package util //nolint:revive // util namespace is intentional for shared helpers
import (
"crypto/rand"

View file

@ -1,29 +1,32 @@
#!/bin/sh
# Exit when any command fails
set -e
# Get the script directory and change to the project root
# Change to repo root
cd "$(dirname "$0")/../"
# Detect the operating system
OS=$(uname -s)
# Set output file name based on the OS
if [[ "$OS" == *"CYGWIN"* || "$OS" == *"MINGW"* || "$OS" == *"MSYS"* ]]; then
OUTPUT="./build/memos.exe"
else
OUTPUT="./build/memos"
fi
# Determine output binary name
case "$OS" in
*CYGWIN*|*MINGW*|*MSYS*)
OUTPUT="./build/memos.exe"
;;
*)
OUTPUT="./build/memos"
;;
esac
echo "Building for $OS..."
# Ensure build directories exist and configure a writable Go build cache
mkdir -p ./build/.gocache ./build/.gomodcache
export GOCACHE="$(pwd)/build/.gocache"
export GOMODCACHE="$(pwd)/build/.gomodcache"
# Build the executable
go build -o "$OUTPUT" ./bin/memos/main.go
go build -o "$OUTPUT" ./bin/memos
# Output the success message
echo "Build successful!"
# Output the command to run
echo "To run the application, execute the following command:"
echo "$OUTPUT --mode dev"

View file

@ -4,30 +4,28 @@ import (
"context"
"github.com/pkg/errors"
"github.com/usememos/gomark"
"github.com/usememos/gomark/ast"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
"github.com/usememos/gomark/renderer"
"github.com/usememos/gomark/restore"
"github.com/usememos/memos/plugin/httpgetter"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
)
func (*APIV1Service) ParseMarkdown(_ context.Context, request *v1pb.ParseMarkdownRequest) (*v1pb.ParseMarkdownResponse, error) {
rawNodes, err := parser.Parse(tokenizer.Tokenize(request.Markdown))
doc, err := gomark.Parse(request.Markdown)
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
nodes := convertFromASTNodes(rawNodes)
nodes := convertFromASTDocument(doc)
return &v1pb.ParseMarkdownResponse{
Nodes: nodes,
}, nil
}
func (*APIV1Service) RestoreMarkdownNodes(_ context.Context, request *v1pb.RestoreMarkdownNodesRequest) (*v1pb.RestoreMarkdownNodesResponse, error) {
markdown := restore.Restore(convertToASTNodes(request.Nodes))
markdown := gomark.Restore(convertToASTDocument(request.Nodes))
return &v1pb.RestoreMarkdownNodesResponse{
Markdown: markdown,
}, nil
@ -35,7 +33,7 @@ func (*APIV1Service) RestoreMarkdownNodes(_ context.Context, request *v1pb.Resto
func (*APIV1Service) StringifyMarkdownNodes(_ context.Context, request *v1pb.StringifyMarkdownNodesRequest) (*v1pb.StringifyMarkdownNodesResponse, error) {
stringRenderer := renderer.NewStringRenderer()
plainText := stringRenderer.Render(convertToASTNodes(request.Nodes))
plainText := stringRenderer.RenderDocument(convertToASTDocument(request.Nodes))
return &v1pb.StringifyMarkdownNodesResponse{
PlainText: plainText,
}, nil
@ -100,7 +98,9 @@ func convertFromASTNode(rawNode ast.Node) *v1pb.Node {
case *ast.Italic:
node.Node = &v1pb.Node_ItalicNode{ItalicNode: &v1pb.ItalicNode{Symbol: n.Symbol, Children: convertFromASTNodes(n.Children)}}
case *ast.BoldItalic:
node.Node = &v1pb.Node_BoldItalicNode{BoldItalicNode: &v1pb.BoldItalicNode{Symbol: n.Symbol, Content: n.Content}}
childDoc := &ast.Document{Children: n.Children}
plain := renderer.NewStringRenderer().RenderDocument(childDoc)
node.Node = &v1pb.Node_BoldItalicNode{BoldItalicNode: &v1pb.BoldItalicNode{Symbol: n.Symbol, Content: plain}}
case *ast.Code:
node.Node = &v1pb.Node_CodeNode{CodeNode: &v1pb.CodeNode{Content: n.Content}}
case *ast.Image:
@ -144,6 +144,13 @@ func convertFromASTNodes(rawNodes []ast.Node) []*v1pb.Node {
return nodes
}
func convertFromASTDocument(doc *ast.Document) []*v1pb.Node {
if doc == nil {
return nil
}
return convertFromASTNodes(doc.Children)
}
func convertTableFromASTNode(node *ast.Table) *v1pb.TableNode {
table := &v1pb.TableNode{
Header: convertFromASTNodes(node.Header),
@ -210,7 +217,11 @@ func convertToASTNode(node *v1pb.Node) ast.Node {
case *v1pb.Node_ItalicNode:
return &ast.Italic{Symbol: n.ItalicNode.Symbol, Children: convertToASTNodes(n.ItalicNode.Children)}
case *v1pb.Node_BoldItalicNode:
return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content}
children := []ast.Node{}
if n.BoldItalicNode.Content != "" {
children = append(children, &ast.Text{Content: n.BoldItalicNode.Content})
}
return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Children: children}
case *v1pb.Node_CodeNode:
return &ast.Code{Content: n.CodeNode.Content}
case *v1pb.Node_ImageNode:
@ -253,6 +264,10 @@ func convertToASTNodes(nodes []*v1pb.Node) []ast.Node {
return rawNodes
}
func convertToASTDocument(nodes []*v1pb.Node) *ast.Document {
return &ast.Document{Children: convertToASTNodes(nodes)}
}
func convertTableToASTNode(node *v1pb.TableNode) *ast.Table {
table := &ast.Table{
Header: convertToASTNodes(node.Header),

View file

@ -10,11 +10,9 @@ import (
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"github.com/usememos/gomark"
"github.com/usememos/gomark/ast"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
"github.com/usememos/gomark/renderer"
"github.com/usememos/gomark/restore"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
@ -711,16 +709,16 @@ func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMe
}
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
doc, err := gomark.Parse(memo.Content)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
}
memopayload.TraverseASTNodes(nodes, func(node ast.Node) {
memopayload.TraverseASTDocument(doc, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldTag {
tag.Content = request.NewTag
}
})
memo.Content = restore.Restore(nodes)
memo.Content = gomark.Restore(doc)
if err := memopayload.RebuildMemoPayload(memo); err != nil {
return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
}
@ -840,12 +838,12 @@ func convertMemoToWebhookPayload(memo *v1pb.Memo) (*webhook.WebhookRequestPayloa
}
func getMemoContentSnippet(content string) (string, error) {
nodes, err := parser.Parse(tokenizer.Tokenize(content))
doc, err := gomark.Parse(content)
if err != nil {
return "", errors.Wrap(err, "failed to parse content")
}
plainText := renderer.NewStringRenderer().Render(nodes)
plainText := renderer.NewStringRenderer().RenderDocument(doc)
if len(plainText) > 64 {
return substring(plainText, 64) + "...", nil
}

View file

@ -8,8 +8,7 @@ import (
"github.com/pkg/errors"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
"github.com/usememos/gomark"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
@ -69,11 +68,13 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
memoMessage.Attachments = append(memoMessage.Attachments, attachmentResponse)
}
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
doc, err := gomark.Parse(memo.Content)
if err != nil {
return nil, errors.Wrap(err, "failed to parse content")
}
memoMessage.Nodes = convertFromASTNodes(nodes)
if doc != nil {
memoMessage.Nodes = convertFromASTNodes(doc.Children)
}
snippet, err := getMemoContentSnippet(memo.Content)
if err != nil {

View file

@ -152,11 +152,11 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
}
func getRSSItemDescription(content string) (string, error) {
nodes, err := gomark.Parse(content)
doc, err := gomark.Parse(content)
if err != nil {
return "", err
}
result := renderer.NewHTMLRenderer().Render(nodes)
result := renderer.NewHTMLRenderer().RenderDocument(doc)
return result, nil
}

View file

@ -6,9 +6,8 @@ import (
"slices"
"github.com/pkg/errors"
"github.com/usememos/gomark"
"github.com/usememos/gomark/ast"
"github.com/usememos/gomark/parser"
"github.com/usememos/gomark/parser/tokenizer"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
@ -73,7 +72,7 @@ func (r *Runner) RunOnce(ctx context.Context) {
}
func RebuildMemoPayload(memo *store.Memo) error {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
doc, err := gomark.Parse(memo.Content)
if err != nil {
return errors.Wrap(err, "failed to parse content")
}
@ -83,7 +82,7 @@ func RebuildMemoPayload(memo *store.Memo) error {
}
tags := []string{}
property := &storepb.MemoPayload_Property{}
TraverseASTNodes(nodes, func(node ast.Node) {
TraverseASTDocument(doc, func(node ast.Node) {
switch n := node.(type) {
case *ast.Tag:
tag := n.Content
@ -109,26 +108,33 @@ func RebuildMemoPayload(memo *store.Memo) error {
return nil
}
func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
func TraverseASTDocument(doc *ast.Document, fn func(ast.Node)) {
if doc == nil {
return
}
traverseASTNodes(doc.Children, fn)
}
func traverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.Heading:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.Blockquote:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.List:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.OrderedListItem:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.UnorderedListItem:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.TaskListItem:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
case *ast.Bold:
TraverseASTNodes(n.Children, fn)
traverseASTNodes(n.Children, fn)
}
}
}