github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/note/markdown.go (about)

     1  package note
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"time"
     7  
     8  	"github.com/cozy/cozy-stack/model/note/custom"
     9  	"github.com/cozy/prosemirror-go/markdown"
    10  	"github.com/cozy/prosemirror-go/model"
    11  	"github.com/yuin/goldmark/ast"
    12  	"github.com/yuin/goldmark/extension"
    13  	extensionast "github.com/yuin/goldmark/extension/ast"
    14  	"github.com/yuin/goldmark/parser"
    15  	"github.com/yuin/goldmark/util"
    16  )
    17  
    18  func markdownSerializer(images []*Image) *markdown.Serializer {
    19  	vanilla := markdown.DefaultSerializer
    20  	nodes := map[string]markdown.NodeSerializerFunc{
    21  		"paragraph": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    22  			for _, mark := range node.Marks {
    23  				if mark.Type.Name == "alignment" {
    24  					switch mark.Attrs["align"] {
    25  					case "center":
    26  						state.Write("[^]{.center}")
    27  					case "end":
    28  						state.Write("[^]{.right}")
    29  					}
    30  				}
    31  			}
    32  			state.RenderInline(node)
    33  			state.CloseBlock(node)
    34  		},
    35  		"text":        vanilla.Nodes["text"],
    36  		"bulletList":  vanilla.Nodes["bullet_list"],
    37  		"orderedList": vanilla.Nodes["ordered_list"],
    38  		"listItem":    vanilla.Nodes["list_item"],
    39  		"decisionList": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    40  			if node.Attrs == nil {
    41  				node.Attrs = map[string]interface{}{}
    42  			}
    43  			node.Attrs["tight"] = true
    44  			state.RenderList(node, "  ", func(_ int) string { return "✍ " })
    45  		},
    46  		"decisionItem": vanilla.Nodes["list_item"],
    47  		"taskList": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    48  			state.RenderList(node, "  ", func(_ int) string { return "- " })
    49  		},
    50  		"taskItem": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    51  			if node.Attrs["state"] == "DONE" {
    52  				state.Write("[X] ")
    53  			} else {
    54  				state.Write("[ ] ")
    55  			}
    56  			state.RenderContent(node)
    57  		},
    58  		"heading":    vanilla.Nodes["heading"],
    59  		"blockquote": vanilla.Nodes["blockquote"],
    60  		"rule":       vanilla.Nodes["horizontal_rule"],
    61  		"hardBreak":  vanilla.Nodes["hard_break"],
    62  		"image":      vanilla.Nodes["image"],
    63  		"codeBlock": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    64  			lang, _ := node.Attrs["language"].(string)
    65  			state.Write("```" + lang + "\n")
    66  			state.Text(node.TextContent(), false)
    67  			state.EnsureNewLine()
    68  			state.Write("```")
    69  			state.CloseBlock(node)
    70  		},
    71  		"panel": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    72  			if typ, ok := node.Attrs["panelType"].(string); ok {
    73  				state.Write(":" + typ + ": ")
    74  			}
    75  			state.RenderContent(node)
    76  		},
    77  		"table": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    78  			var attrs string
    79  			if node.Attrs["layout"] == "wide" {
    80  				attrs += ` layout="wide"`
    81  			} else if node.Attrs["layout"] == "full-width" {
    82  				attrs += ` layout="full-width"`
    83  			}
    84  			if node.Attrs["isNumberColumnEnabled"] == true {
    85  				attrs += " number=true"
    86  			}
    87  			state.Write("________________________________________{.table" + attrs + "}\n\n")
    88  			state.RenderContent(node)
    89  			state.EnsureNewLine()
    90  			state.Write("________________________________________{.tableEnd}\n")
    91  			state.CloseBlock(node)
    92  		},
    93  		"tableRow": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
    94  			state.Write("________________________________________{.tableRow}\n\n")
    95  			state.RenderContent(node)
    96  			state.EnsureNewLine()
    97  			state.CloseBlock(node)
    98  		},
    99  		"tableHeader": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   100  			attrs := cellMarkup(node)
   101  			state.Write("____________________{.tableHeader" + attrs + "}\n\n")
   102  			state.RenderContent(node)
   103  		},
   104  		"tableCell": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   105  			attrs := cellMarkup(node)
   106  			state.Write("____________________{.tableCell" + attrs + "}\n\n")
   107  			state.RenderContent(node)
   108  		},
   109  		"status": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   110  			if txt, ok := node.Attrs["text"].(string); ok {
   111  				state.Write("[")
   112  				state.Text(txt)
   113  				state.Write("]")
   114  				color, _ := node.Attrs["color"].(string)
   115  				id, _ := node.Attrs["localId"].(string)
   116  				state.Text(fmt.Sprintf(`{.status color="%s" localId="%s"}`, color, id))
   117  			}
   118  		},
   119  		"date": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   120  			if ts, ok := node.Attrs["timestamp"].(string); ok {
   121  				if seconds, err := strconv.ParseInt(ts, 10, 64); err == nil {
   122  					txt := time.Unix(seconds/1000, 0).Format("2006-01-02")
   123  					state.Write("[")
   124  					state.Text(txt)
   125  					state.Write("]")
   126  					state.Text(fmt.Sprintf(`{.date ts="%s"}`, ts))
   127  				}
   128  			}
   129  		},
   130  		"mediaSingle": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   131  			state.RenderContent(node)
   132  		},
   133  		"media": func(state *markdown.SerializerState, node, _parent *model.Node, _index int) {
   134  			var alt string
   135  			src, _ := node.Attrs["url"].(string)
   136  			for _, img := range images {
   137  				if img.DocID == src {
   138  					alt = img.Name
   139  					img.seen = true
   140  				}
   141  			}
   142  			state.Write(fmt.Sprintf("![%s](%s)\n", state.Esc(alt), state.Esc(src)))
   143  		},
   144  	}
   145  	marks := map[string]markdown.MarkSerializerSpec{
   146  		"em":          vanilla.Marks["em"],
   147  		"strong":      vanilla.Marks["strong"],
   148  		"link":        vanilla.Marks["link"],
   149  		"code":        vanilla.Marks["code"],
   150  		"strike":      {Open: "~~", Close: "~~", ExpelEnclosingWhitespace: true},
   151  		"indentation": {Open: "    ", Close: "", ExpelEnclosingWhitespace: true},
   152  		"breakout":    {Open: "", Close: "", ExpelEnclosingWhitespace: true},
   153  		"underline":   {Open: "[", Close: "]{.underlined}", ExpelEnclosingWhitespace: true},
   154  		"subsup": {
   155  			Open: "[",
   156  			Close: func(state *markdown.SerializerState, mark *model.Mark, parent *model.Node, index int) string {
   157  				typ, _ := mark.Attrs["type"].(string)
   158  				return fmt.Sprintf("]{.%s}", typ)
   159  			},
   160  		},
   161  		"textColor": {
   162  			Open: "[",
   163  			Close: func(state *markdown.SerializerState, mark *model.Mark, parent *model.Node, index int) string {
   164  				color, _ := mark.Attrs["color"].(string)
   165  				return fmt.Sprintf(`]{.color rgb="%s"}`, color)
   166  			},
   167  		},
   168  	}
   169  	return markdown.NewSerializer(nodes, marks)
   170  }
   171  
   172  func cellMarkup(node *model.Node) string {
   173  	var attrs string
   174  	if color, ok := node.Attrs["background"].(string); ok && color[0] == '#' {
   175  		attrs += fmt.Sprintf(` background="%s"`, color)
   176  	}
   177  	if span, ok := node.Attrs["rowspan"].(float64); ok && span > 1 {
   178  		attrs += fmt.Sprintf(" rowspan=%d", int(span))
   179  	}
   180  	if span, ok := node.Attrs["colspan"].(float64); ok && span > 1 {
   181  		attrs += fmt.Sprintf(" colspan=%d", int(span))
   182  	}
   183  	return attrs
   184  }
   185  
   186  func isTableCell(item *markdown.StackItem) bool {
   187  	name := item.Type.Name
   188  	return name == "tableHeader" || name == "tableCell"
   189  }
   190  
   191  func cellAttributes(node ast.Node) map[string]interface{} {
   192  	background, ok := node.AttributeString("background")
   193  	if !ok {
   194  		background = nil
   195  	}
   196  	colspan, ok := node.AttributeString("colspan")
   197  	if !ok {
   198  		colspan = 1
   199  	}
   200  	rowspan, ok := node.AttributeString("rowspan")
   201  	if !ok {
   202  		rowspan = 1
   203  	}
   204  	return map[string]interface{}{
   205  		"background": background,
   206  		"colspan":    colspan,
   207  		"rowspan":    rowspan,
   208  	}
   209  }
   210  
   211  func markdownNodeMapper() markdown.NodeMapper {
   212  	vanilla := markdown.DefaultNodeMapper
   213  	return markdown.NodeMapper{
   214  		// Blocks
   215  		ast.KindDocument:  vanilla[ast.KindDocument],
   216  		ast.KindParagraph: vanilla[ast.KindParagraph],
   217  		ast.KindHeading:   vanilla[ast.KindHeading],
   218  		ast.KindList: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   219  			if entering {
   220  				isATaskList := false
   221  				listItem := node.FirstChild()
   222  				if listItem != nil {
   223  					isATaskList = true
   224  					for listItem != nil {
   225  						para := listItem.FirstChild()
   226  						if para == nil {
   227  							isATaskList = false
   228  							break
   229  						}
   230  						checkbox := para.FirstChild()
   231  						if checkbox == nil || checkbox.Kind() != extensionast.KindTaskCheckBox {
   232  							isATaskList = false
   233  							break
   234  						}
   235  						listItem = listItem.NextSibling()
   236  					}
   237  				}
   238  				if isATaskList {
   239  					typ, err := state.Schema.NodeType("taskList")
   240  					if err != nil {
   241  						return err
   242  					}
   243  					state.OpenNode(typ, nil)
   244  					return nil
   245  				}
   246  				return vanilla[ast.KindList](state, node, entering)
   247  			}
   248  			_, err := state.CloseNode()
   249  			return err
   250  		},
   251  		ast.KindListItem: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   252  			inTaskList := false
   253  			for _, item := range state.Stack {
   254  				if item.Type.Name == "taskList" {
   255  					inTaskList = true
   256  				}
   257  			}
   258  			if inTaskList {
   259  				if entering {
   260  					typ, err := state.Schema.NodeType("taskItem")
   261  					if err != nil {
   262  						return err
   263  					}
   264  					state.OpenNode(typ, nil)
   265  					return nil
   266  				} else {
   267  					// taskItem have their content directly inside them, no paragraphs
   268  					item := state.Top()
   269  					paragraphs := item.Content
   270  					item.Content = nil
   271  					for _, paragraph := range paragraphs {
   272  						item.Content = append(item.Content, paragraph.Content.Content...)
   273  					}
   274  					_, err := state.CloseNode()
   275  					return err
   276  				}
   277  			}
   278  			return vanilla[ast.KindListItem](state, node, entering)
   279  		},
   280  		ast.KindTextBlock:       vanilla[ast.KindTextBlock],
   281  		ast.KindBlockquote:      vanilla[ast.KindBlockquote],
   282  		ast.KindCodeBlock:       vanilla[ast.KindCodeBlock],
   283  		ast.KindFencedCodeBlock: vanilla[ast.KindFencedCodeBlock],
   284  		ast.KindThematicBreak:   vanilla[ast.KindThematicBreak],
   285  		custom.KindDecisionList: markdown.GenericBlockHandler("decisionList"),
   286  		custom.KindDecisionItem: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   287  			if entering {
   288  				typ, err := state.Schema.NodeType("decisionItem")
   289  				if err != nil {
   290  					return err
   291  				}
   292  				state.OpenNode(typ, nil)
   293  			} else {
   294  				// decisionItem have their content directly inside them, no paragraphs
   295  				item := state.Top()
   296  				paragraphs := item.Content
   297  				item.Content = nil
   298  				for _, paragraph := range paragraphs {
   299  					item.Content = append(item.Content, paragraph.Content.Content...)
   300  				}
   301  				if _, err := state.CloseNode(); err != nil {
   302  					return err
   303  				}
   304  			}
   305  			return nil
   306  		},
   307  		custom.KindPanel: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   308  			if entering {
   309  				typ, err := state.Schema.NodeType("panel")
   310  				if err != nil {
   311  					return err
   312  				}
   313  				attrs := map[string]interface{}{
   314  					"panelType": node.(*custom.Panel).PanelType,
   315  				}
   316  				state.OpenNode(typ, attrs)
   317  			} else {
   318  				if _, err := state.CloseNode(); err != nil {
   319  					return err
   320  				}
   321  			}
   322  			return nil
   323  		},
   324  		custom.KindTable: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   325  			tableType, ok := node.AttributeString("class")
   326  			if entering || !ok {
   327  				return nil
   328  			}
   329  
   330  			var attrs map[string]interface{}
   331  			switch tableType {
   332  			case "table":
   333  				number, ok := node.AttributeString("number")
   334  				if !ok {
   335  					number = false
   336  				}
   337  				layout, ok := node.AttributeString("layout")
   338  				if !ok {
   339  					layout = "default"
   340  				}
   341  				attrs = map[string]interface{}{
   342  					"__autosize":            false,
   343  					"isNumberColumnEnabled": number,
   344  					"layout":                layout,
   345  				}
   346  			case "tableEnd":
   347  				if isTableCell(state.Top()) {
   348  					if _, err := state.CloseNode(); err != nil { // Cell
   349  						return err
   350  					}
   351  					if _, err := state.CloseNode(); err != nil { // Row
   352  						return err
   353  					}
   354  				}
   355  				if _, err := state.CloseNode(); err != nil { // Table
   356  					return err
   357  				}
   358  				return nil
   359  			case "tableRow":
   360  				if isTableCell(state.Top()) {
   361  					if _, err := state.CloseNode(); err != nil { // Cell
   362  						return err
   363  					}
   364  					if _, err := state.CloseNode(); err != nil { // Row
   365  						return err
   366  					}
   367  				}
   368  			case "tableHeader":
   369  				if isTableCell(state.Top()) {
   370  					if _, err := state.CloseNode(); err != nil {
   371  						return err
   372  					}
   373  				}
   374  				attrs = cellAttributes(node)
   375  			case "tableCell":
   376  				if isTableCell(state.Top()) {
   377  					if _, err := state.CloseNode(); err != nil {
   378  						return err
   379  					}
   380  				}
   381  				attrs = cellAttributes(node)
   382  			default:
   383  				return nil
   384  			}
   385  			typ, err := state.Schema.NodeType(tableType.(string))
   386  			if err != nil {
   387  				return err
   388  			}
   389  			state.OpenNode(typ, attrs)
   390  			return nil
   391  		},
   392  
   393  		// Inlines
   394  		ast.KindText:     vanilla[ast.KindText],
   395  		ast.KindString:   vanilla[ast.KindString],
   396  		ast.KindAutoLink: vanilla[ast.KindAutoLink],
   397  		ast.KindLink:     vanilla[ast.KindLink],
   398  		ast.KindImage: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   399  			if entering {
   400  				return nil
   401  			}
   402  			paragraph := state.Pop()
   403  			var text string
   404  			for _, n := range paragraph.Content {
   405  				if n.Text != nil {
   406  					text += *n.Text
   407  				}
   408  			}
   409  			typeMS, err := state.Schema.NodeType("mediaSingle")
   410  			if err != nil {
   411  				return err
   412  			}
   413  			state.OpenNode(typeMS, map[string]interface{}{"layout": "center"})
   414  			typ, err := state.Schema.NodeType("media")
   415  			if err != nil {
   416  				return err
   417  			}
   418  			n := node.(*ast.Image)
   419  			attrs := map[string]interface{}{
   420  				"url":  string(n.Destination),
   421  				"alt":  text,
   422  				"id":   "",
   423  				"type": "external",
   424  			}
   425  			state.OpenNode(typ, attrs)
   426  			if _, err := state.CloseNode(); err != nil { // media
   427  				return err
   428  			}
   429  			if _, err := state.CloseNode(); err != nil { // mediaSingle
   430  				return err
   431  			}
   432  			state.OpenNode(paragraph.Type, paragraph.Attrs)
   433  			return nil
   434  		},
   435  		ast.KindCodeSpan:               vanilla[ast.KindCodeSpan],
   436  		ast.KindEmphasis:               vanilla[ast.KindEmphasis],
   437  		extensionast.KindStrikethrough: vanilla[extensionast.KindStrikethrough],
   438  		extensionast.KindTaskCheckBox: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   439  			if entering {
   440  				if len(state.Stack) <= 2 {
   441  					return nil
   442  				}
   443  				grandparent := state.Stack[len(state.Stack)-2]
   444  				if grandparent.Type.Name == "taskItem" {
   445  					s := "TODO"
   446  					n := node.(*extensionast.TaskCheckBox)
   447  					if n.IsChecked {
   448  						s = "DONE"
   449  					}
   450  					if grandparent.Attrs == nil {
   451  						grandparent.Attrs = map[string]interface{}{}
   452  					}
   453  					grandparent.Attrs["state"] = s
   454  				}
   455  			}
   456  			return nil
   457  		},
   458  		custom.KindSpan: func(state *markdown.MarkdownParseState, node ast.Node, entering bool) error {
   459  			text := node.(*custom.Span).Value
   460  
   461  			var markType, nodeType string
   462  			var attrs map[string]interface{}
   463  			if class, ok := node.AttributeString("class"); ok {
   464  				switch class {
   465  				case "underlined":
   466  					markType = "underline"
   467  				case "sub":
   468  					markType = "subsup"
   469  					attrs = map[string]interface{}{"type": "sub"}
   470  				case "sup":
   471  					markType = "subsup"
   472  					attrs = map[string]interface{}{"type": "sup"}
   473  				case "color":
   474  					if color, ok := node.AttributeString("rgb"); ok {
   475  						markType = "textColor"
   476  						attrs = map[string]interface{}{"color": color}
   477  					}
   478  				case "status":
   479  					if color, ok := node.AttributeString("color"); ok {
   480  						id, _ := node.AttributeString("localId")
   481  						nodeType = "status"
   482  						attrs = map[string]interface{}{
   483  							"color":   color,
   484  							"localId": id,
   485  							"text":    text,
   486  						}
   487  					}
   488  				case "date":
   489  					nodeType = "date"
   490  					ts, _ := node.AttributeString("ts")
   491  					attrs = map[string]interface{}{"timestamp": ts}
   492  				case "center", "right":
   493  					markType = "alignment"
   494  					align := "center"
   495  					if class == "right" {
   496  						align = "end"
   497  					}
   498  					attrs = map[string]interface{}{"align": align}
   499  				}
   500  			}
   501  
   502  			if markType != "" {
   503  				typ, err := state.Schema.MarkType(markType)
   504  				if err != nil {
   505  					return err
   506  				}
   507  				mark := typ.Create(attrs)
   508  				if entering {
   509  					state.OpenMark(mark)
   510  					state.AddText(text)
   511  				} else {
   512  					state.CloseMark(mark)
   513  				}
   514  			} else if nodeType != "" {
   515  				if entering {
   516  					typ, err := state.Schema.NodeType(nodeType)
   517  					if err != nil {
   518  						return err
   519  					}
   520  					state.OpenNode(typ, attrs)
   521  				} else {
   522  					if _, err := state.CloseNode(); err != nil {
   523  						return err
   524  					}
   525  				}
   526  			} else {
   527  				if entering {
   528  					state.AddText(text)
   529  				}
   530  				return nil
   531  			}
   532  			return nil
   533  		},
   534  	}
   535  }
   536  
   537  func markdownParser() parser.Parser {
   538  	return parser.NewParser(
   539  		parser.WithBlockParsers(
   540  			util.Prioritized(custom.NewTableParser(), 50),
   541  			util.Prioritized(parser.NewSetextHeadingParser(), 100),
   542  			util.Prioritized(parser.NewThematicBreakParser(), 200),
   543  			util.Prioritized(parser.NewListParser(), 300),
   544  			util.Prioritized(parser.NewListItemParser(), 350),
   545  			util.Prioritized(custom.NewDecisionListParser(), 400),
   546  			util.Prioritized(custom.NewDecisionItemParser(), 450),
   547  			util.Prioritized(parser.NewCodeBlockParser(), 500),
   548  			util.Prioritized(parser.NewATXHeadingParser(), 600),
   549  			util.Prioritized(parser.NewFencedCodeBlockParser(), 700),
   550  			util.Prioritized(parser.NewBlockquoteParser(), 800),
   551  			util.Prioritized(custom.NewPanelParser(), 900),
   552  			util.Prioritized(parser.NewParagraphParser(), 1000),
   553  		),
   554  		parser.WithInlineParsers(
   555  			util.Prioritized(extension.NewTaskCheckBoxParser(), 0),
   556  			util.Prioritized(custom.NewSpanParser(), 50),
   557  			util.Prioritized(parser.NewCodeSpanParser(), 100),
   558  			util.Prioritized(parser.NewLinkParser(), 200),
   559  			util.Prioritized(parser.NewAutoLinkParser(), 300),
   560  			util.Prioritized(parser.NewEmphasisParser(), 400),
   561  			util.Prioritized(extension.NewStrikethroughParser(), 500),
   562  		),
   563  		parser.WithParagraphTransformers(
   564  			util.Prioritized(parser.LinkReferenceParagraphTransformer, 100),
   565  		),
   566  	)
   567  }