diff --git a/go.mod b/go.mod index f3ae559eee389aca65d87d1a33193844f18987d7..0bddd18d7893028bc97bf1f9535249db58e8819c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/cozy/goexif2 v0.0.0-20200819113101-00e1cc8cc9d3 github.com/cozy/gomail v0.0.0-20170313100128-1395d9a6a6c0 github.com/cozy/httpcache v0.0.0-20210224123405-3f334f841945 - github.com/cozy/prosemirror-go v0.4.12 + github.com/cozy/prosemirror-go v0.5.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 github.com/dustin/go-humanize v1.0.0 @@ -44,7 +44,7 @@ require ( github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.7.0 github.com/ugorji/go/codec v1.2.6 - github.com/yuin/goldmark v1.4.3 + github.com/yuin/goldmark v1.4.4 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d golang.org/x/net v0.0.0-20211020060615-d418f374d309 diff --git a/go.sum b/go.sum index 445f0c1ea59a3a1e0c2bc41b7697d37f7ac9beb5..59ef4c815fb1b89fc830f9bbd84976beacf52799 100644 --- a/go.sum +++ b/go.sum @@ -99,8 +99,8 @@ github.com/cozy/gomail v0.0.0-20170313100128-1395d9a6a6c0 h1:bQVNaGvnUI7m8J8k3hk github.com/cozy/gomail v0.0.0-20170313100128-1395d9a6a6c0/go.mod h1:DlX8Rq7OKA0F9I1e0tz6+PCOXkKZ/l6aD+bWxCC6Qfo= github.com/cozy/httpcache v0.0.0-20210224123405-3f334f841945 h1:EfeD2CzaZclMHyFxSuaA1BTfqVTLaFwqlASiNNil4nE= github.com/cozy/httpcache v0.0.0-20210224123405-3f334f841945/go.mod h1:rLnjIcybyvs+PoCzi4+GmpOVp0+q+qdcuZKnKUKJoF4= -github.com/cozy/prosemirror-go v0.4.12 h1:Gp07Ix7euU7mB0N8lq8kLD3NvdBu6xLAWEZQe46cTU8= -github.com/cozy/prosemirror-go v0.4.12/go.mod h1:v15sN6s5qGHCgb1cgVGk2ZZ53lLzefVmFVwX53E3Lb8= +github.com/cozy/prosemirror-go v0.5.0 h1:4VhyxSvfTfq5/L8WOjdW85aQRc07yvlcznPDxmjJtKA= +github.com/cozy/prosemirror-go v0.5.0/go.mod h1:pK1bwqX1BJQ+7QB54pY44j9Dd7rAzorFWZwDRhAu5KM= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -478,8 +478,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.3 h1:eTEYYWtQLjK7+WK45Tk81OTkp/0UvAyqUj8flU0nTO4= -github.com/yuin/goldmark v1.4.3/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= +github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= diff --git a/model/note/from_markdown.go b/model/note/from_markdown.go deleted file mode 100644 index ec6a0365749125e7713b8d8184f4dbb5e87c577d..0000000000000000000000000000000000000000 --- a/model/note/from_markdown.go +++ /dev/null @@ -1,385 +0,0 @@ -package note - -import ( - "errors" - "fmt" - "strings" - - "github.com/cozy/prosemirror-go/model" - "github.com/yuin/goldmark/ast" - extensionast "github.com/yuin/goldmark/extension/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" -) - -func maybeMerge(a, b *model.Node) *model.Node { - if a.IsText() && b.IsText() && model.SameMarkSet(a.Marks, b.Marks) { - return a.WithText(*a.Text + *b.Text) - } - return nil -} - -// MarkdownParseState is an object used to track the context of a running -// parse. -type MarkdownParseState struct { - Source []byte - Schema *model.Schema - Root *model.Node - Stack []*StackItem - Marks []*model.Mark - ParentMarks []*model.Mark -} - -type StackItem struct { - Type *model.NodeType - Attrs map[string]interface{} - Content []*model.Node -} - -type NodeMapper map[ast.NodeKind]NodeMapperFunc - -type NodeMapperFunc func(state *MarkdownParseState, node ast.Node, entering bool) error - -func (state *MarkdownParseState) Top() *StackItem { - if len(state.Stack) == 0 { - panic(errors.New("Empty stack")) - } - last := len(state.Stack) - 1 - return state.Stack[last] -} - -func (state *MarkdownParseState) Push(node *model.Node) { - item := state.Top() - item.Content = append(item.Content, node) -} - -func (state *MarkdownParseState) Pop() *StackItem { - if len(state.Stack) == 0 { - panic(errors.New("Empty stack")) - } - last := len(state.Stack) - 1 - item := state.Stack[last] - state.Stack = state.Stack[:last] - return item -} - -// AddText adds the given text to the current position in the document, using -// the current marks as styling. -func (state *MarkdownParseState) AddText(text string) { - item := state.Top() - node := state.Schema.Text(text, state.Marks) - if len(item.Content) > 0 { - last := item.Content[len(item.Content)-1] - if merged := maybeMerge(last, node); merged != nil { - item.Content[len(item.Content)-1] = merged - return - } - } - item.Content = append(item.Content, node) -} - -// OpenMark adds the given mark to the set of active marks. -func (state *MarkdownParseState) OpenMark(mark *model.Mark) { - state.Marks = mark.AddToSet(state.Marks) -} - -// CloseMark removes the given mark from the set of active marks. -func (state *MarkdownParseState) CloseMark(mark *model.Mark) { - state.Marks = mark.RemoveFromSet(state.Marks) -} - -func (state *MarkdownParseState) AddParentMark(mark *model.Mark) { - state.ParentMarks = mark.AddToSet(state.ParentMarks) -} - -// AddNode adds a node at the current position. -func (state *MarkdownParseState) AddNode(typ *model.NodeType, attrs map[string]interface{}, content interface{}) (*model.Node, error) { - node, err := typ.CreateAndFill(attrs, content, state.Marks) - if node == nil { - return nil, err - } - state.Push(node) - return node, nil -} - -// OpenNode wraps subsequent content in a node of the given type. -func (state *MarkdownParseState) OpenNode(typ *model.NodeType, attrs map[string]interface{}) { - item := &StackItem{Type: typ, Attrs: attrs} - state.Stack = append(state.Stack, item) -} - -// CloseNode closes and returns the node that is currently on top of the stack. -func (state *MarkdownParseState) CloseNode() (*model.Node, error) { - if len(state.ParentMarks) > 0 { - state.Marks = state.ParentMarks - state.ParentMarks = model.NoMarks - defer func() { state.Marks = model.NoMarks }() - } else if len(state.Marks) > 0 { - state.Marks = model.NoMarks - } - info := state.Pop() - return state.AddNode(info.Type, info.Attrs, info.Content) -} - -// ParseMarkdown parses a string as [CommonMark](http://commonmark.org/) -// markup, and create a ProseMirror document as prescribed by this parser's -// rules. -func ParseMarkdown(parser parser.Parser, funcs NodeMapper, source []byte, schema *model.Schema) (*model.Node, error) { - tree := parser.Parse(text.NewReader(source)) - state := &MarkdownParseState{Source: source, Schema: schema} - err := ast.Walk(tree, func(node ast.Node, entering bool) (ast.WalkStatus, error) { - if fn, ok := funcs[node.Kind()]; ok { - if err := fn(state, node, entering); err != nil { - return ast.WalkStop, err - } - } else { - return ast.WalkStop, fmt.Errorf("Node kind %s not supported by markdown parser", node.Kind()) - } - return ast.WalkContinue, nil - }) - if err != nil { - return nil, err - } - if state.Root == nil { - return nil, errors.New("Cannot build prosemirror content") - } - return state.Root, nil -} - -func GenericBlockHandler(nodeType string) NodeMapperFunc { - return func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - typ, err := state.Schema.NodeType(nodeType) - if err != nil { - return err - } - state.OpenNode(typ, nil) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - } -} - -func GenericMarkHandler(markType string) NodeMapperFunc { - return func(state *MarkdownParseState, node ast.Node, entering bool) error { - typ, err := state.Schema.MarkType(markType) - if err != nil { - return err - } - var attrs map[string]interface{} - mark := typ.Create(attrs) - if entering { - state.OpenMark(mark) - } else { - state.CloseMark(mark) - } - return nil - } -} - -func WithoutTrailingNewline(node ast.Node, source []byte) string { - var lines []string - segments := node.Lines() - for i := 0; i < segments.Len(); i++ { - segment := segments.At(i) - line := segment.Value(source) - lines = append(lines, string(line)) - } - str := strings.Join(lines, "") - return strings.TrimSuffix(str, "\n") -} - -// DefaultNodeMapper is a parser parsing unextended -// [CommonMark](http://commonmark.org/), without inline HTML, and producing a -// document in the basic schema. -var DefaultNodeMapper = NodeMapper{ - // Blocks - ast.KindDocument: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - typ, err := state.Schema.NodeType(state.Schema.Spec.TopNode) - if err != nil { - return err - } - state.OpenNode(typ, nil) - } else { - info := state.Pop() - node, err := info.Type.CreateAndFill(info.Attrs, info.Content, state.Marks) - if err != nil { - return err - } - state.Root = node - } - return nil - }, - ast.KindParagraph: GenericBlockHandler("paragraph"), - ast.KindHeading: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - typ, err := state.Schema.NodeType("heading") - if err != nil { - return err - } - level := node.(*ast.Heading).Level - attrs := map[string]interface{}{"level": level} - state.OpenNode(typ, attrs) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - }, - ast.KindList: func(state *MarkdownParseState, node ast.Node, entering bool) error { - nodeType := "bulletList" - if node.(*ast.List).IsOrdered() { - nodeType = "orderedList" - } - if entering { - typ, err := state.Schema.NodeType(nodeType) - if err != nil { - return err - } - state.OpenNode(typ, nil) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - }, - ast.KindListItem: GenericBlockHandler("listItem"), - ast.KindTextBlock: GenericBlockHandler("paragraph"), - ast.KindBlockquote: GenericBlockHandler("blockquote"), - ast.KindCodeBlock: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - node := node.(*ast.CodeBlock) - typ, err := state.Schema.NodeType("codeBlock") - if err != nil { - return err - } - state.OpenNode(typ, nil) - state.AddText(WithoutTrailingNewline(node, state.Source)) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - }, - ast.KindFencedCodeBlock: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - node := node.(*ast.FencedCodeBlock) - typ, err := state.Schema.NodeType("codeBlock") - if err != nil { - return err - } - lang := node.Language(state.Source) - attrs := map[string]interface{}{ - "language": string(lang), - } - state.OpenNode(typ, attrs) - state.AddText(WithoutTrailingNewline(node, state.Source)) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - }, - ast.KindThematicBreak: GenericBlockHandler("rule"), - - // Inlines - ast.KindText: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - segment := node.(*ast.Text).Segment - content := segment.Value(state.Source) - state.AddText(string(content)) - } - return nil - }, - ast.KindString: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - content := node.(*ast.String).Value - state.AddText(string(content)) - } - return nil - }, - ast.KindAutoLink: func(state *MarkdownParseState, node ast.Node, entering bool) error { - typ, err := state.Schema.MarkType("link") - if err != nil { - return err - } - n := node.(*ast.AutoLink) - url := string(n.URL(state.Source)) - attrs := map[string]interface{}{"href": url} - mark := typ.Create(attrs) - if entering { - state.OpenMark(mark) - state.AddText(url) - } else { - state.CloseMark(mark) - } - return nil - }, - ast.KindLink: func(state *MarkdownParseState, node ast.Node, entering bool) error { - typ, err := state.Schema.MarkType("link") - if err != nil { - return err - } - n := node.(*ast.Link) - attrs := map[string]interface{}{ - "href": string(n.Destination), - "title": string(n.Title), - } - mark := typ.Create(attrs) - if entering { - state.OpenMark(mark) - } else { - state.CloseMark(mark) - } - return nil - }, - ast.KindImage: func(state *MarkdownParseState, node ast.Node, entering bool) error { - if entering { - typ, err := state.Schema.NodeType("image") - if err != nil { - return err - } - n := node.(*ast.Image) - attrs := map[string]interface{}{ - "src": string(n.Destination), - "title": string(n.Title), - } - state.OpenNode(typ, attrs) - } else { - if _, err := state.CloseNode(); err != nil { - return err - } - } - return nil - }, - ast.KindCodeSpan: GenericMarkHandler("code"), - ast.KindEmphasis: func(state *MarkdownParseState, node ast.Node, entering bool) error { - var typ *model.MarkType - var err error - if node.(*ast.Emphasis).Level == 2 { - typ, err = state.Schema.MarkType("strong") - } else { - typ, err = state.Schema.MarkType("em") - } - if err != nil { - return err - } - var attrs map[string]interface{} - mark := typ.Create(attrs) - if entering { - state.OpenMark(mark) - } else { - state.CloseMark(mark) - } - return nil - }, - extensionast.KindStrikethrough: GenericMarkHandler("strike"), -} diff --git a/model/note/import.go b/model/note/import.go index 95ba7b7127bd5bc61c81e6ce3bce4bbd9830a21b..65efe8dec14a1c83f6e0990d5bb9f27ee0bb8a3a 100644 --- a/model/note/import.go +++ b/model/note/import.go @@ -13,6 +13,7 @@ import ( "github.com/cozy/cozy-stack/model/vfs" "github.com/cozy/cozy-stack/pkg/consts" "github.com/cozy/cozy-stack/pkg/filetype" + "github.com/cozy/prosemirror-go/markdown" "github.com/cozy/prosemirror-go/model" "github.com/gofrs/uuid" ) @@ -163,7 +164,7 @@ func parseFile(r io.Reader, schema *model.Schema) (*model.Node, error) { } parser := markdownParser() funcs := markdownNodeMapper() - return ParseMarkdown(parser, funcs, buf, schema) + return markdown.ParseMarkdown(parser, funcs, buf, schema) } func isTar(buf []byte) bool { diff --git a/model/note/markdown.go b/model/note/markdown.go index 99cfd0441a57e6e77a2d98653fc5c8f53d2e83f7..e6a780da6d14dd158c4487153582eb1ffd599c94 100644 --- a/model/note/markdown.go +++ b/model/note/markdown.go @@ -164,7 +164,7 @@ func cellMarkup(node *model.Node) string { return attrs } -func isTableCell(item *StackItem) bool { +func isTableCell(item *markdown.StackItem) bool { name := item.Type.Name return name == "tableHeader" || name == "tableCell" } @@ -189,9 +189,9 @@ func cellAttributes(node ast.Node) map[string]interface{} { } } -func markdownNodeMapper() NodeMapper { - vanilla := DefaultNodeMapper - return NodeMapper{ +func markdownNodeMapper() markdown.NodeMapper { + vanilla := markdown.DefaultNodeMapper + return markdown.NodeMapper{ // Blocks ast.KindDocument: vanilla[ast.KindDocument], ast.KindParagraph: vanilla[ast.KindParagraph], @@ -203,7 +203,7 @@ func markdownNodeMapper() NodeMapper { ast.KindCodeBlock: vanilla[ast.KindCodeBlock], ast.KindFencedCodeBlock: vanilla[ast.KindFencedCodeBlock], ast.KindThematicBreak: vanilla[ast.KindThematicBreak], - custom.KindPanel: func(state *MarkdownParseState, node ast.Node, entering bool) error { + custom.KindPanel: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error { if entering { typ, err := state.Schema.NodeType("panel") if err != nil { @@ -220,7 +220,7 @@ func markdownNodeMapper() NodeMapper { } return nil }, - custom.KindTable: func(state *MarkdownParseState, node ast.Node, entering bool) error { + custom.KindTable: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error { tableType, ok := node.AttributeString("class") if entering || !ok { return nil @@ -294,7 +294,7 @@ func markdownNodeMapper() NodeMapper { ast.KindString: vanilla[ast.KindString], ast.KindAutoLink: vanilla[ast.KindAutoLink], ast.KindLink: vanilla[ast.KindLink], - ast.KindImage: func(state *MarkdownParseState, node ast.Node, entering bool) error { + ast.KindImage: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error { if entering { return nil } @@ -334,7 +334,7 @@ func markdownNodeMapper() NodeMapper { ast.KindCodeSpan: vanilla[ast.KindCodeSpan], ast.KindEmphasis: vanilla[ast.KindEmphasis], extensionast.KindStrikethrough: vanilla[extensionast.KindStrikethrough], - custom.KindSpan: func(state *MarkdownParseState, node ast.Node, entering bool) error { + custom.KindSpan: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error { text := node.(*custom.Span).Value var markType, nodeType string