From 0f8372685d2914e3b0af13ca9ec3dcb20b6468a9 Mon Sep 17 00:00:00 2001 From: codemaestro64 Date: Sun, 25 Jul 2021 17:52:15 +0100 Subject: [PATCH] correctly render markdown widgets --- ui/page/overview_page.go | 22 +- ui/renderers/const.go | 23 +++ ui/renderers/html.go | 120 +++++++++-- ui/renderers/markdown.go | 424 ++++++++++++++++++++++++++++++++++++++- ui/renderers/renderer.go | 384 +++++++++++++++++++++++++++++------ ui/renderers/style.go | 254 ++++++++++++++++++++++- ui/renderers/table.go | 6 +- 7 files changed, 1134 insertions(+), 99 deletions(-) create mode 100644 ui/renderers/const.go diff --git a/ui/page/overview_page.go b/ui/page/overview_page.go index 71aebd44b..7f8721f47 100644 --- a/ui/page/overview_page.go +++ b/ui/page/overview_page.go @@ -16,6 +16,7 @@ import ( "github.com/planetdecred/dcrlibwallet" "github.com/planetdecred/godcr/ui/decredmaterial" + "github.com/planetdecred/godcr/ui/renderers" "github.com/planetdecred/godcr/ui/values" "github.com/planetdecred/godcr/wallet" ) @@ -141,8 +142,27 @@ func (pg *OverviewPage) loadTransactions() { pg.transactions = transactions } +func (pg *OverviewPage) Layout(gtx C) D { + sc := `1. First ordered list item with really long text. First ordered list item with really long text. First ordered list item with really long text. First ordered list item with really long text +2. Another item +3. Actual numbers don't matter, just that it's a number +4. And another item. + +Welcome to the cong +` + + m := renderers.RenderMarkdown(gtx, pg.theme, sc) + l, _ := m.Layout() + + return uniformPadding(gtx, func(gtx C) D { + return pg.listContainer.Layout(gtx, len(l), func(gtx C, i int) D { + return layout.UniformInset(values.MarginPadding5).Layout(gtx, l[i]) + }) + }) +} + // Layout lays out the entire content for overview pg. -func (pg *OverviewPage) Layout(gtx layout.Context) layout.Dimensions { +func (pg *OverviewPage) LayoutMain(gtx layout.Context) layout.Dimensions { pg.queue = gtx if pg.WL.Info.LoadedWallets == 0 { return uniformPadding(gtx, func(gtx C) D { diff --git a/ui/renderers/const.go b/ui/renderers/const.go new file mode 100644 index 000000000..d9221d305 --- /dev/null +++ b/ui/renderers/const.go @@ -0,0 +1,23 @@ +package renderers + +const ( + openTagPrefix = "[@" + openTagSuffix = "@]" + closeTag = "[/]" + + italicsTagName = "i" + strongTagName = "strong" + emphTagName = "emph" + strikeTagName = "strike" + blockQuoteTagName = "blockquote" + orderedListTagName = "ol" + unorderedListTagName = "ul" + listItemTagName = "li" + + h1TagName = "h1" + h2TagName = "h2" + h3TagName = "h3" + h4TagName = "h4" + h5TagName = "h5" + h6TagName = "h6" +) diff --git a/ui/renderers/html.go b/ui/renderers/html.go index 5bf259d41..29bef3a53 100644 --- a/ui/renderers/html.go +++ b/ui/renderers/html.go @@ -10,14 +10,12 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/PuerkitoBio/goquery" "github.com/gomarkdown/markdown/ast" - "github.com/gomarkdown/markdown/parser" + //"github.com/gomarkdown/markdown/parser" "github.com/planetdecred/godcr/ui/decredmaterial" ) -type HTMLRenderer struct { - doc ast.Node +type HTMLProvider struct { container *layout.List - *Renderer } var ( @@ -30,7 +28,83 @@ const ( closeStyleTag = "{/@}" ) -func RenderHTML(html string, theme *decredmaterial.Theme) *HTMLRenderer { +func RenderHTML(html string, theme *decredmaterial.Theme) *HTMLProvider { + htmlProvider := &HTMLProvider{} + + converter := md.NewConverter("", true, nil) + docStr, err := converter.ConvertString(htmlProvider.prepare(html)) + if err != nil { + fmt.Println(err) + return &HTMLProvider{} + } + + /**htmlProvider := &HTMLProvider{} + + converter := md.NewConverter("", true, nil) + + docStr, err := converter.ConvertString(docStr) + if err != nil { + fmt.Println(err) + return r + } + + return newRenderer(theme, &HTMLProvider{})**/ + newNodeWalker(docStr, htmlProvider) + + return htmlProvider +} + +func (p *HTMLProvider) prepareBlockQuote(node *ast.BlockQuote, entering bool) { + +} + +func (p *HTMLProvider) prepareList(node *ast.List, entering bool) { + +} + +func (p *HTMLProvider) prepareListItem(node *ast.ListItem, entering bool) { + +} + +func (p *HTMLProvider) prepareParagraph(node *ast.Paragraph, entering bool) { + +} + +func (p *HTMLProvider) prepareHeading(node *ast.Heading, entering bool) { + +} + +func (p *HTMLProvider) prepareStrong(node *ast.Strong, entering bool) { + +} +func (p *HTMLProvider) prepareDel(node *ast.Del, entering bool) { + +} +func (p *HTMLProvider) prepareEmph(node *ast.Emph, entering bool) { + +} +func (p *HTMLProvider) prepareLink(node *ast.Link, entering bool) { + +} + +func (p *HTMLProvider) prepareHorizontalRule(node *ast.HorizontalRule, entering bool) { + +} + +func (p *HTMLProvider) prepareText(node *ast.Text, entering bool) { + +} +func (p *HTMLProvider) prepareTable(node *ast.Table, entering bool) { + +} +func (p *HTMLProvider) prepareTableCell(node *ast.TableCell, entering bool) { + +} +func (p *HTMLProvider) prepareTableRow(node *ast.TableRow, entering bool) { + +} + +/**func RenderHTML(html string, theme *decredmaterial.Theme) *HTMLRenderer { converter := md.NewConverter("", true, nil) r := &HTMLRenderer{ @@ -66,8 +140,9 @@ func RenderHTML(html string, theme *decredmaterial.Theme) *HTMLRenderer { return r } +**/ -func (r *HTMLRenderer) prepareHTML(html string) string { +func (r *HTMLProvider) prepare(html string) string { //html = strings.Replace(html, "
", " \n\n ", -1) doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) @@ -100,7 +175,7 @@ func (r *HTMLRenderer) prepareHTML(html string) string { return doc.Text() } -func (r *HTMLRenderer) prepareItalic(node *goquery.Selection) { +func (r *HTMLProvider) prepareItalic(node *goquery.Selection) { style, ok := node.Attr("style") if ok { style += "; font-style: italic" @@ -111,7 +186,7 @@ func (r *HTMLRenderer) prepareItalic(node *goquery.Selection) { node.ReplaceWithHtml(fmt.Sprintf(`%s`, style, node.Text())) } -func (r *HTMLRenderer) prepareBold(node *goquery.Selection) { +func (r *HTMLProvider) prepareBold(node *goquery.Selection) { style, ok := node.Attr("style") if ok { style += "; font-weight: bold" @@ -122,7 +197,7 @@ func (r *HTMLRenderer) prepareBold(node *goquery.Selection) { node.ReplaceWithHtml(fmt.Sprintf(`%s`, style, node.Text())) } -func (r *HTMLRenderer) prepareFont(node *goquery.Selection) { +func (r *HTMLProvider) prepareFont(node *goquery.Selection) { style, _ := node.Attr("style") if style != "" { style += "; " @@ -139,11 +214,11 @@ func (r *HTMLRenderer) prepareFont(node *goquery.Selection) { node.ReplaceWithHtml(fmt.Sprintf(`%s`, style, node.Text())) } -func (r *HTMLRenderer) prepareBreak(node *goquery.Selection) { +func (r *HTMLProvider) prepareBreak(node *goquery.Selection) { node.ReplaceWithHtml("\n\n") } -func (r *HTMLRenderer) mapToString(m map[string]string) string { +func (r *HTMLProvider) mapToString(m map[string]string) string { b := new(bytes.Buffer) for key, value := range m { fmt.Fprintf(b, "%s=\"%s\"\n", key, value) @@ -151,7 +226,7 @@ func (r *HTMLRenderer) mapToString(m map[string]string) string { return b.String() } -func (r *HTMLRenderer) getStyleMap(node *goquery.Selection) map[string]string { +func (r *HTMLProvider) getStyleMap(node *goquery.Selection) map[string]string { if styleStr, ok := node.Attr("style"); ok { spl := strings.Split(styleStr, ";") styleMap := map[string]string{} @@ -169,7 +244,7 @@ func (r *HTMLRenderer) getStyleMap(node *goquery.Selection) map[string]string { return map[string]string{} } -func (r *HTMLRenderer) styleMapToString(m map[string]string) string { +func (r *HTMLProvider) styleMapToString(m map[string]string) string { str := "" for k, v := range m { str += "##" + k + "--" + v @@ -178,14 +253,14 @@ func (r *HTMLRenderer) styleMapToString(m map[string]string) string { return str } -func (r *HTMLRenderer) traverse(node *goquery.Selection, parentStyle map[string]string) { +func (r *HTMLProvider) traverse(node *goquery.Selection, parentStyle map[string]string) { node.Children().Each(func(_ int, s *goquery.Selection) { newStyle := r.setNodeStyle(s, parentStyle) r.traverse(s, newStyle) }) } -func (r *HTMLRenderer) isBlockElement(element string) bool { +func (r *HTMLProvider) isBlockElement(element string) bool { for i := range blockEls { if element == blockEls[i] { return true @@ -195,7 +270,7 @@ func (r *HTMLRenderer) isBlockElement(element string) bool { return false } -func (r *HTMLRenderer) setNodeStyle(node *goquery.Selection, parentStyle map[string]string) map[string]string { +func (r *HTMLProvider) setNodeStyle(node *goquery.Selection, parentStyle map[string]string) map[string]string { styleMap := r.getStyleMap(node) for key, val := range parentStyle { if _, ok := styleMap[key]; !ok { @@ -214,19 +289,20 @@ func (r *HTMLRenderer) setNodeStyle(node *goquery.Selection, parentStyle map[str return styleMap } -func (r *HTMLRenderer) parse() []byte { +func (r *HTMLProvider) parse() []byte { var buf bytes.Buffer - ast.WalkFunc(r.doc, func(node ast.Node, entering bool) ast.WalkStatus { + /**ast.WalkFunc(r.doc, func(node ast.Node, entering bool) ast.WalkStatus { return r.RenderNode(&buf, node, entering) - }) + })**/ return buf.Bytes() } -func (r *HTMLRenderer) Layout(gtx C) D { - w, _ := r.Renderer.Layout() +func (r *HTMLProvider) Layout(gtx C) D { + /**w, _ := r.Renderer.Layout() return r.container.Layout(gtx, len(w), func(gtx C, i int) D { return w[i](gtx) - }) + })**/ + return D{} } diff --git a/ui/renderers/markdown.go b/ui/renderers/markdown.go index 71f741e1f..a0f7f613a 100644 --- a/ui/renderers/markdown.go +++ b/ui/renderers/markdown.go @@ -1,26 +1,433 @@ package renderers import ( + "fmt" "strings" + "unicode" "gioui.org/layout" + "gioui.org/widget" - md "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/parser" + //md "github.com/gomarkdown/markdown" + //"github.com/gomarkdown/markdown/parser" + "github.com/gomarkdown/markdown/ast" "github.com/planetdecred/godcr/ui/decredmaterial" ) +const ( + bulletUnicode = "\u2022" + linkTag = "[[link" + linkSpacer = "@@@@" +) + type ( C = layout.Context D = layout.Dimensions ) -type MarkdownRenderer struct { - *Renderer +type MarkdownProvider struct { + containers []layout.Widget + theme *decredmaterial.Theme + listItemNumber int // should be negative when not rendering a list + links map[string]*widget.Clickable + table *table + + stringBuilder strings.Builder + tagStack []string +} + +func RenderMarkdown(gtx C, theme *decredmaterial.Theme, source string) *MarkdownProvider { + mdProvider := &MarkdownProvider{ + theme: theme, + listItemNumber: -1, + } + source = mdProvider.prepare(source) + + newNodeWalker(source, mdProvider).walk() + return mdProvider +} + +func (*MarkdownProvider) prepare(doc string) string { + d := strings.Replace(doc, ":|", "------:|", -1) + d = strings.Replace(d, "-|", "------|", -1) + d = strings.Replace(d, "|-", "|------", -1) + d = strings.Replace(d, "|:-", "|:------", -1) + + return d +} + +func (m *MarkdownProvider) Layout() ([]layout.Widget, map[string]*widget.Clickable) { + return m.containers, map[string]*widget.Clickable{} +} + +func (p *MarkdownProvider) prepareBlockQuote(node *ast.BlockQuote, entering bool) { + p.openOrCloseTag(blockQuoteTagName, entering) +} + +func (p *MarkdownProvider) prepareStrong(node *ast.Strong, entering bool) { + p.openOrCloseTag(strongTagName, entering) +} + +func (p *MarkdownProvider) prepareDel(node *ast.Del, entering bool) { + p.openOrCloseTag(strikeTagName, entering) +} + +func (p *MarkdownProvider) prepareEmph(node *ast.Emph, entering bool) { + p.openOrCloseTag(emphTagName, entering) +} + +func (p *MarkdownProvider) prepareHorizontalRule(node *ast.HorizontalRule, entering bool) { + p.containers = append(p.containers, renderHorizontalLine(p.theme)) +} + +func (p *MarkdownProvider) prepareList(node *ast.List, entering bool) { + if next := ast.GetNextNode(node); !entering && next != nil { + _, parentIsListItem := node.GetParent().(*ast.ListItem) + _, nextIsList := next.(*ast.List) + if !nextIsList && !parentIsListItem { + p.renderEmptyLine(true) + p.listItemNumber = -1 + + } + } +} + +func (p *MarkdownProvider) prepareListItem(node *ast.ListItem, entering bool) { + var prefix string + + if entering { + p.listItemNumber++ + if node.ListFlags&ast.ListTypeOrdered != 0 { + // numbered list + prefix = fmt.Sprintf("%d. ", p.listItemNumber+1) + } else if node.ListFlags&ast.ListTypeDefinition != 0 { + prefix = "" + } else { + //prefix = bulletUnicode + " " + } + + p.openTag(listItemTagName) + p.stringBuilder.WriteString(prefix) + } +} + +func (p *MarkdownProvider) prepareParagraph(node *ast.Paragraph, entering bool) { + if !entering { + p.render() + p.renderEmptyLine(false) + } else { + p.stringBuilder.WriteString(string(node.Literal)) + } +} + +func (p *MarkdownProvider) prepareHeading(node *ast.Heading, entering bool) { + if entering { + var tag string + switch node.Level { + case 1: + tag = h1TagName + case 2: + tag = h2TagName + case 3: + tag = h3TagName + case 4: + tag = h4TagName + case 5: + tag = h5TagName + case 6: + tag = h6TagName + } + p.openTag(tag) + } else { + p.closeTag() + p.render() + p.renderEmptyLine(false) + } +} + +func (p *MarkdownProvider) prepareLink(node *ast.Link, entering bool) { + dest := string(node.Destination) + text := string(ast.GetFirstChild(node).AsLeaf().Literal) + + if p.links == nil { + p.links = map[string]*widget.Clickable{} + } + + if _, ok := p.links[dest]; !ok { + p.links[dest] = new(widget.Clickable) + } + + // fix a bug that causes the link to be written to the builder before this is called + content := p.stringBuilder.String() + p.stringBuilder.Reset() + + parts := strings.Split(content, " ") + parts = parts[:len(parts)-1] + for i := range parts { + p.stringBuilder.WriteString(parts[i] + " ") + } + + word := linkTag + linkSpacer + dest + linkSpacer + strings.Replace(text, " ", "---", -1) + p.stringBuilder.WriteString(word) +} + +func (p *MarkdownProvider) prepareText(node *ast.Text, entering bool) { + if string(node.Literal) == "\n" { + return + } + + content := string(node.Literal) + if shouldCleanText(node) || p.listItemNumber > -1 { + content = removeLineBreak(content) + } + p.stringBuilder.WriteString(content) + if p.listItemNumber > -1 { + p.closeTag() + } +} + +func (p *MarkdownProvider) prepareTable(node *ast.Table, entering bool) { + if entering { + p.table = newTable(p.theme) + } else { + p.containers = append(p.containers, p.table.render()) + p.table = nil + } +} + +func (p *MarkdownProvider) prepareTableCell(node *ast.TableCell, entering bool) { + content := p.stringBuilder.String() + p.stringBuilder.Reset() + + align := cellAlignLeft + switch node.Align { + case ast.TableAlignmentRight: + align = cellAlignRight + case ast.TableAlignmentCenter: + align = cellAlignCenter + } + + var alignment cellAlign + if node.IsHeader { + alignment = align + } else { + alignment = cellAlignCopyHeader + } + p.table.addCell(content, alignment, node.IsHeader) +} + +func (p *MarkdownProvider) prepareTableRow(node *ast.TableRow, entering bool) { + if _, ok := node.Parent.(*ast.TableBody); ok && entering { + p.table.startNextRow() + } + if _, ok := node.Parent.(*ast.TableFooter); ok && entering { + p.table.startNextRow() + } +} + +func (p *MarkdownProvider) pushTag(tagName string) { + p.tagStack = append(p.tagStack, tagName) +} + +func (p *MarkdownProvider) popTag() { + if len(p.tagStack) > 0 { + p.tagStack = p.tagStack[:len(p.tagStack)-1] + } +} + +func (p *MarkdownProvider) renderEmptyLine(isList bool) { + p.containers = append(p.containers, renderEmptyLine(p.theme, isList)) +} + +func (p *MarkdownProvider) renderLineBreak() layout.Widget { + return func(gtx C) D { + dims := p.theme.Body2("").Layout(gtx) + dims.Size.Y = dims.Size.Y + 5 + return dims + } +} + +func (p *MarkdownProvider) renderCurrentText(txt string) layout.Widget { + lbl := p.theme.Body1(txt) + var container layout.Widget + + for index := range p.tagStack { + i := index + switch p.tagStack[i] { + case listItemTagName: + container = renderListItem(lbl, p.theme) + case italicsTagName, emphTagName: + lbl = setStyle(lbl, italicsTagName) + case strongTagName: + lbl = setWeight(lbl, strongTagName) + case strikeTagName: + container = renderStrike(lbl, p.theme) + case blockQuoteTagName: + container = renderBlockQuote(lbl, p.theme) + case h1TagName, h2TagName, h3TagName, h4TagName, h5TagName, h6TagName: + lbl = getHeading(txt, p.tagStack[i], p.theme) + default: + + } + } + + if container == nil { + return lbl.Layout + } + + return container +} + +func (p *MarkdownProvider) render() { + content := p.stringBuilder.String() + p.stringBuilder.Reset() + + var wdgts []layout.Widget + var isGettingTagName bool + var isClosingBlock bool + var isInBlock bool + var currentTag string + var currText string + + //fmt.Println(content) + + for index := range content { + i := index + curr := content[i] + + if curr == openTagPrefix[0] && getNextChar(content, i) == openTagPrefix[1] { + wdgts = append(wdgts, p.renderCurrentText(currText)) + currText = "" + + isGettingTagName = true + isInBlock = true + currentTag = string(curr) + continue + } else if !isInBlock { + currStr := string(curr) + currText += currStr + + if i+1 == len(content) || currStr == "" || currStr == " " { + if strings.HasPrefix(currText, linkTag) { + wdgts = append(wdgts, p.getLinkWidget(currText)) + } else { + lbl := p.theme.Body1(currText) + wdgts = append(wdgts, lbl.Layout) + } + + currText = "" + } + continue + } + + if isGettingTagName { + currentTag += string(curr) + if curr == openTagSuffix[1] { + isGettingTagName = false + currentTag = strings.Replace(currentTag, openTagPrefix, "", -1) + currentTag = strings.Replace(currentTag, openTagSuffix, "", -1) + p.pushTag(currentTag) + currentTag = "" + } + continue + } + + if curr == closeTag[0] && getNextChar(content, i) == closeTag[1] { + isClosingBlock = true + continue + } + + if isClosingBlock { + if curr == closeTag[2] { + wdgts = append(wdgts, p.renderCurrentText(currText)) + p.popTag() + currText = "" + isClosingBlock = false + isInBlock = false + } + continue + } + currText += string(curr) + } + + wdgt := func(gtx C) D { + return decredmaterial.GridWrap{ + Axis: layout.Horizontal, + Alignment: layout.Start, + }.Layout(gtx, len(wdgts), func(gtx C, i int) D { + if wdgts[i] == nil { + return D{} + } + + return wdgts[i](gtx) + }) + } + p.containers = append(p.containers, wdgt) +} + +func (p *MarkdownProvider) openOrCloseTag(tagName string, entering bool) { + if entering { + p.openTag(tagName) + } else { + p.closeTag() + } +} +func (p *MarkdownProvider) openTag(tagName string) { + tag := openTagPrefix + tagName + openTagSuffix + p.stringBuilder.WriteString(tag) +} + +func (p *MarkdownProvider) closeTag() { + p.stringBuilder.WriteString(closeTag) +} + +func shouldCleanText(node ast.Node) bool { + for node != nil { + switch node.(type) { + case *ast.BlockQuote: + return false + + case *ast.Heading, *ast.Image, *ast.Link, + *ast.TableCell, *ast.Document, *ast.ListItem: + return true + } + node = node.GetParent() + } + + return false +} + +func removeLineBreak(text string) string { + lines := strings.Split(text, "\n") + + if len(lines) <= 1 { + return text + } + + for i, l := range lines { + switch i { + case 0: + lines[i] = strings.TrimRightFunc(l, unicode.IsSpace) + case len(lines) - 1: + lines[i] = strings.TrimLeftFunc(l, unicode.IsSpace) + default: + lines[i] = strings.TrimFunc(l, unicode.IsSpace) + } + } + + return strings.Join(lines, " ") +} + +func getNextChar(content string, currIndex int) byte { + if currIndex+2 <= len(content) { + return content[currIndex+1] + } + + return 0 } -func RenderMarkdown(gtx layout.Context, theme *decredmaterial.Theme, source string) *MarkdownRenderer { - extensions := parser.NoIntraEmphasis // Ignore emphasis markers inside words +/**func RenderMarkdown(gtx layout.Context, theme *decredmaterial.Theme, source string) interface{} { + /**extensions := parser.NoIntraEmphasis // Ignore emphasis markers inside words extensions |= parser.Tables // Parse tables extensions |= parser.FencedCode // Parse fenced code blocks extensions |= parser.Autolink // Detect embedded URLs that are not explicitly marked @@ -35,7 +442,7 @@ func RenderMarkdown(gtx layout.Context, theme *decredmaterial.Theme, source stri p := parser.NewWithExtensions(extensions) r := &MarkdownRenderer{ - newRenderer(theme, false), + //newRenderer(theme, false), } source = r.prepareDocForTable(source) @@ -44,6 +451,7 @@ func RenderMarkdown(gtx layout.Context, theme *decredmaterial.Theme, source stri md.Render(nodes, r.Renderer) return r + return nil } func (r *MarkdownRenderer) prepareDocForTable(doc string) string { @@ -53,4 +461,4 @@ func (r *MarkdownRenderer) prepareDocForTable(doc string) string { d = strings.Replace(d, "|:-", "|:------", -1) return d -} +}**/ diff --git a/ui/renderers/renderer.go b/ui/renderers/renderer.go index 0891ec1e8..2cd5c7d27 100644 --- a/ui/renderers/renderer.go +++ b/ui/renderers/renderer.go @@ -1,26 +1,185 @@ package renderers import ( - "fmt" - "image" + //"fmt" + //"image" "io" - "regexp" - "strings" - "unicode" - - "gioui.org/layout" - "gioui.org/text" - "gioui.org/widget" - "gioui.org/widget/material" - + //"reflect" + //"regexp" + //"strings" + //"unicode" + + //"gioui.org/layout" + //"gioui.org/text" + //"gioui.org/unit" + //"gioui.org/widget" + //"gioui.org/widget/material" + + //md "github.com/JohannesKaufmann/html-to-markdown" + md "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" - "github.com/planetdecred/godcr/ui/decredmaterial" + "github.com/gomarkdown/markdown/parser" + //"github.com/planetdecred/godcr/ui/decredmaterial" ) -type labelFunc func(string) decredmaterial.Label +type renderer interface { + prepareText(node *ast.Text, entering bool) + prepareBlockQuote(node *ast.BlockQuote, entering bool) + prepareList(node *ast.List, entering bool) + prepareListItem(node *ast.ListItem, entering bool) + prepareParagraph(node *ast.Paragraph, entering bool) + prepareHeading(node *ast.Heading, entering bool) + prepareStrong(node *ast.Strong, entering bool) + prepareDel(node *ast.Del, entering bool) + prepareEmph(node *ast.Emph, entering bool) + prepareLink(node *ast.Link, entering bool) + prepareTable(node *ast.Table, entering bool) + prepareTableCell(node *ast.TableCell, entering bool) + prepareTableRow(node *ast.TableRow, entering bool) + prepareHorizontalRule(node *ast.HorizontalRule, entering bool) + //layout() ([]layout.Widget, map[string]*widget.Clickable) +} + +type nodeWalker struct { + rootNode ast.Node + renderer renderer +} + +func newNodeWalker(doc string, renderer renderer) *nodeWalker { + extensions := parser.NoIntraEmphasis // Ignore emphasis markers inside words + extensions |= parser.Tables // Parse tables + extensions |= parser.FencedCode // Parse fenced code blocks + extensions |= parser.Autolink // Detect embedded URLs that are not explicitly marked + extensions |= parser.Strikethrough // Strikethrough text using ~~test~~ + extensions |= parser.SpaceHeadings // Be strict about prefix heading rules + //extensions |= parser.HeadingIDs // specify heading IDs with {#id} + extensions |= parser.BackslashLineBreak // Translate trailing backslashes into line breaks + extensions |= parser.DefinitionLists // Parse definition lists + extensions |= parser.LaxHTMLBlocks // more in HTMLBlock, less in HTMLSpan + //extensions |= parser.NoEmptyLineBeforeBlock // no need for new line before a list + extensions |= parser.Attributes + //extensions |= parser.EmptyLinesBreakList + extensions |= parser.Mmark + extensions |= parser.LaxHTMLBlocks + + p := parser.NewWithExtensions(extensions) + + return &nodeWalker{ + rootNode: md.Parse([]byte(doc), p), + renderer: renderer, + } +} + +func (nw *nodeWalker) walk() { + md.Render(nw.rootNode, nw) +} + +func (nw *nodeWalker) walkerFunc() { + +} + +func (nw *nodeWalker) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { + switch node := node.(type) { + case *ast.Document: + //fmt.Println(string(node.Literal)) + case *ast.BlockQuote: + nw.renderer.prepareBlockQuote(node, entering) + case *ast.List: + nw.renderer.prepareList(node, entering) + case *ast.ListItem: + nw.renderer.prepareListItem(node, entering) + case *ast.Paragraph: + nw.renderer.prepareParagraph(node, entering) + case *ast.Heading: + nw.renderer.prepareHeading(node, entering) + case *ast.Strong: + nw.renderer.prepareStrong(node, entering) + case *ast.Del: + nw.renderer.prepareDel(node, entering) + case *ast.Emph: + nw.renderer.prepareEmph(node, entering) + case *ast.Link: + if !entering { + nw.renderer.prepareLink(node, entering) + return ast.SkipChildren + } + case *ast.Text: + nw.renderer.prepareText(node, entering) + case *ast.HorizontalRule: + nw.renderer.prepareHorizontalRule(node, entering) + case *ast.Table: + nw.renderer.prepareTable(node, entering) + case *ast.TableRow: + nw.renderer.prepareTableRow(node, entering) + case *ast.TableCell: + if !entering { + nw.renderer.prepareTableCell(node, entering) + } + } + return ast.GoToNext +} + +/**func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus { + switch node := node.(type) { + case *ast.Document: + // Nothing to do + case *ast.BlockQuote: + r.renderBlockQuote(entering) + case *ast.List: + // extra new line at the end of a list *if* next is not a list + if next := ast.GetNextNode(node); !entering && next != nil { + _, parentIsListItem := node.GetParent().(*ast.ListItem) + _, nextIsList := next.(*ast.List) + if !nextIsList && !parentIsListItem { + r.renderEmptyLine() + } + } + case *ast.ListItem: + r.renderList(node, entering) + case *ast.Paragraph: + if !entering { + r.renderParagraph() + } + case *ast.Heading: + if !entering { + r.renderHeading(node.Level, true) + } + case *ast.Strong: + r.renderStrong(entering) + case *ast.Del: + r.renderDel(entering) + case *ast.Emph: + r.renderEmph(entering) + case *ast.Link: + if !entering { + r.renderLink(node) + return ast.SkipChildren + } + case *ast.Text: + r.renderText(node) + case *ast.Table: + r.renderTable(entering) + case *ast.TableCell: + if !entering { + r.renderTableCell(node) + } + case *ast.TableRow: + r.renderTableRow(node, entering) + } + + return ast.GoToNext +} +**/ + +func (*nodeWalker) RenderHeader(w io.Writer, node ast.Node) {} + +func (*nodeWalker) RenderFooter(w io.Writer, node ast.Node) {} + +/**type labelFunc func(string) decredmaterial.Label type Renderer struct { theme *decredmaterial.Theme + provider provider isList bool isListItem bool @@ -46,13 +205,13 @@ var ( textBeforeCloseRegexp = regexp.MustCompile("(.*){/#}") ) -func newRenderer(theme *decredmaterial.Theme, isHTML bool) *Renderer { +/**func newRenderer(theme *decredmaterial.Theme, isHTML bool) *Renderer { return &Renderer{ theme: theme, isHTML: isHTML, } } - +/** func (r *Renderer) pad() string { return strings.Repeat(" ", r.leftPad) + strings.Join(r.padAccumulator, "") } @@ -70,7 +229,7 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal case *ast.Document: // Nothing to do case *ast.BlockQuote: - + r.renderBlockQuote(entering) case *ast.List: // extra new line at the end of a list *if* next is not a list if next := ast.GetNextNode(node); !entering && next != nil { @@ -91,7 +250,11 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal r.renderHeading(node.Level, true) } case *ast.Strong: - r.renderStrong() + r.renderStrong(entering) + case *ast.Del: + r.renderDel(entering) + case *ast.Emph: + r.renderEmph(entering) case *ast.Link: if !entering { r.renderLink(node) @@ -112,16 +275,59 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal return ast.GoToNext } -func (r *Renderer) renderStrong() { - label := r.theme.Body1("") - label.Font.Weight = text.Bold +func (r *Renderer) openMarkdownTag(tagName string) { + tag := openTagPrefix + tagName + openTagSuffix + r.stringBuilder.WriteString(tag) +} + +func (r *Renderer) closeMarkdownTag() { + r.stringBuilder.WriteString(closeTag) +} + +func (r *Renderer) renderBlockQuote(entering bool) { + if r.isHTML { + return + } else if entering { + r.openMarkdownTag(blockQuoteTagName) + } else { + r.closeMarkdownTag() + } +} - r.renderWords(label) +func (r *Renderer) renderStrong(entering bool) { + if r.isHTML { + label := r.theme.Body1("") + label.Font.Weight = text.Bold + r.renderWords(label) + } else if entering { + r.openMarkdownTag(strongTagName) + } else { + r.closeMarkdownTag() + } +} + +func (r *Renderer) renderEmph(entering bool) { + if r.isHTML { + return + } else if entering { + r.openMarkdownTag(emphTagName) + } else { + r.closeMarkdownTag() + } +} + +func (r *Renderer) renderDel(entering bool) { + if r.isHTML { + return + } else if entering { + r.openMarkdownTag(strikeTagName) + } else { + r.closeMarkdownTag() + } } func (r *Renderer) renderParagraph() { r.renderWords(r.theme.Body1("")) - // add dummy widget for new line r.renderEmptyLine() } @@ -198,62 +404,112 @@ func (r *Renderer) renderWords(lbl decredmaterial.Label) { r.renderMarkdown(lbl) } -func (r *Renderer) renderMarkdown(lbl decredmaterial.Label) { - content := r.stringBuilder.String() - if strings.TrimSpace(r.prefix) != "" && strings.TrimSpace(content) == "" { - return +func (r *Renderer) getLabel(lbl decredmaterial.Label, text string) decredmaterial.Label { + l := lbl + l.Text = text + if r.isHTML { + l = r.styleHTMLLabel(l) + } else { + } + return l +} + +func (r *Renderer) getMarkdownWidget(lbl decredmaterial.Label, text string) layout.Widget { + l := lbl + l.Text = text + + return r.getMarkdownWidgetAndStyle(l) +} +func (r *Renderer) renderMarkdown(lbl decredmaterial.Label) { + content := r.stringBuilder.String() r.stringBuilder.Reset() - labelText := lbl.Text - words := strings.Fields(content) - words = append([]string{r.prefix}, words...) - r.prefix = "" + var wdgts []layout.Widget + var isGettingTagName bool + var isClosingBlock bool + var currentTag string + var currText string + + for i := range content { + curr := content[i] + + if curr == openTagPrefix[0] && r.getNextChar(content, i) == openTagPrefix[1] { + wdgts = append(wdgts,r.getMarkdownWidget(lbl, currText)) + currText = "" + + isGettingTagName = true + currentTag = string(curr) + continue + } + + if isGettingTagName { + currentTag += string(curr) + if curr == openTagSuffix[1] { + isGettingTagName = false + currentTag = strings.Replace(currentTag, openTagPrefix, "", -1) + currentTag = strings.Replace(currentTag, openTagSuffix, "", -1) + r.addStyleGroupFromTagName(currentTag) + currentTag = "" + } + continue + } + + if curr == closeTag[0] && r.getNextChar(content, i) == closeTag[1] { + isClosingBlock = true + continue + } + + if isClosingBlock { + if curr == closeTag[2] { + wdgts = append(wdgts,r.getMarkdownWidget(lbl, currText)) + r.removeLastStyleGroup() + currText = "" + isClosingBlock = false + } + continue + } + + currText += string(curr) + } wdgt := func(gtx C) D { return decredmaterial.GridWrap{ Axis: layout.Horizontal, Alignment: layout.Start, - }.Layout(gtx, len(words), func(gtx C, i int) D { - if strings.HasPrefix(words[i], linkTag) { - return r.getLinkWidget(gtx, words[i]) + }.Layout(gtx, len(wdgts), func(gtx C, i int) D { + if wdgts[i] == nil { + return D{} } - word := words[i] + " " - if i == 0 { - word = labelText + " " + words[i] - } - lbl.Text = word - return lbl.Layout(gtx) + return wdgts[i](gtx) }) } + r.containers = append(r.containers, wdgt) - var container layout.Widget - if r.isListItem { - container = func(gtx C) D { - return layout.Flex{}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return D{ - Size: image.Point{ - X: 10, - }, - } - }), - layout.Flexed(1, wdgt), - ) - } - } else { - container = wdgt - } - r.containers = append(r.containers, container) } -func (r *Renderer) getLabel(lbl decredmaterial.Label, text string) decredmaterial.Label { - l := lbl - l.Text = text - l = r.styleLabel(l) - return l +func (r *Renderer) strikeLabel(label decredmaterial.Label) layout.Widget { + return func(gtx C) D { + var dims D + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + dims = label.Layout(gtx) + return dims + }), + layout.Expanded(func(gtx C) D { + return layout.Inset{ + Top: unit.Dp((float32(dims.Size.Y) / float32(2))), + }.Layout(gtx, func(gtx C) D { + l := r.theme.Separator() + l.Color = label.Color + l.Width = dims.Size.X + return l.Layout(gtx) + }) + }), + ) + } } func (r *Renderer) renderHTML(lbl decredmaterial.Label) { @@ -308,7 +564,7 @@ func (r *Renderer) renderHTML(lbl decredmaterial.Label) { if isClosingStyle && curr == halfCloseStyleTag[1] { isClosingStyle = false inStyleBlock = false - r.addStyleGroup(currStyle) + r.addHTMLStyleGroup(currStyle) currStyle = "" } } @@ -462,4 +718,4 @@ func (r *Renderer) getLinkWidget(gtx layout.Context, linkWord string) D { lbl.Color = r.theme.Color.Primary return lbl.Layout(gtx) }) -} \ No newline at end of file +}**/ diff --git a/ui/renderers/style.go b/ui/renderers/style.go index 1062e86c3..2b23e5dea 100644 --- a/ui/renderers/style.go +++ b/ui/renderers/style.go @@ -1,13 +1,186 @@ package renderers import ( - "image/color" + //"fmt" + //"image/color" "strings" + "gioui.org/layout" "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget/material" "github.com/planetdecred/godcr/ui/decredmaterial" ) +func setStyle(lbl decredmaterial.Label, style string) decredmaterial.Label { + switch style { + case italicsTagName, emphTagName: + lbl.Font.Style = text.Italic + } + + return lbl +} + +func setWeight(lbl decredmaterial.Label, weight string) decredmaterial.Label { + var w text.Weight + + switch weight { + case "normal": + w = text.Normal + case "medium": + w = text.Medium + case "bold", "strong": + w = text.Bold + default: + w = lbl.Font.Weight + } + + lbl.Font.Weight = w + return lbl +} + +func getHeading(txt string, tagName string, theme *decredmaterial.Theme) decredmaterial.Label { + var lblWdgt func(string) decredmaterial.Label + + switch tagName { + case h5TagName: + lblWdgt = theme.H5 + case h6TagName: + lblWdgt = theme.H6 + default: + lblWdgt = theme.H4 + } + + return lblWdgt(txt) +} + +func renderStrike(lbl decredmaterial.Label, theme *decredmaterial.Theme) layout.Widget { + return func(gtx C) D { + var dims D + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + dims = lbl.Layout(gtx) + return dims + }), + layout.Expanded(func(gtx C) D { + return layout.Inset{ + Top: unit.Dp((float32(dims.Size.Y) / float32(2))), + }.Layout(gtx, func(gtx C) D { + l := theme.Separator() + l.Color = lbl.Color + l.Width = dims.Size.X + return l.Layout(gtx) + }) + }), + ) + } +} + +func renderBlockQuote(lbl decredmaterial.Label, theme *decredmaterial.Theme) layout.Widget { + words := strings.Fields(lbl.Text) + + return func(gtx C) D { + var dims D + + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + l := theme.SeparatorVertical(dims.Size.Y, 10) + l.Color = theme.Color.Gray + return l.Layout(gtx) + }), + layout.Rigid(func(gtx C) D { + dims = layout.Inset{ + Left: unit.Dp(4), + }.Layout(gtx, func(gtx C) D { + return decredmaterial.GridWrap{ + Axis: layout.Horizontal, + Alignment: layout.Start, + }.Layout(gtx, len(words), func(gtx C, i int) D { + lbl.Text = words[i] + " " + return lbl.Layout(gtx) + }) + }) + + return dims + }), + ) + } +} + +func (p *MarkdownProvider) getLinkWidget(linkWord string) layout.Widget { + parts := strings.Split(linkWord, linkSpacer) + + return func(gtx C) D { + gtx.Constraints.Max.X = gtx.Constraints.Max.X - 200 + return material.Clickable(gtx, p.links[parts[1]], func(gtx C) D { + lbl := p.theme.Body2(strings.Replace(parts[2], "---", " ", -1) + " ") + lbl.Color = p.theme.Color.Primary + return lbl.Layout(gtx) + }) + } +} + +/**func (p *MarkdownProvider) getLinkWidget(gtx layout.Context, linkWord string) D { + parts := strings.Split(linkWord, linkSpacer) + + gtx.Constraints.Max.X = gtx.Constraints.Max.X - 200 + return material.Clickable(gtx, p.links[parts[1]], func(gtx C) D { + lbl := p.theme.Body2(parts[2] + " ") + lbl.Color = p.theme.Color.Primary + return lbl.Layout(gtx) + }) +} +**/ + +func renderHorizontalLine(theme *decredmaterial.Theme) layout.Widget { + l := theme.Separator() + l.Width = 1 + return l.Layout +} + +func renderEmptyLine(theme *decredmaterial.Theme, isList bool) layout.Widget { + var padding = -5 + + if isList { + padding = -10 + } + + return func(gtx C) D { + dims := theme.Body2("").Layout(gtx) + dims.Size.Y = dims.Size.Y + padding + return dims + } +} + +func renderListItem(lbl decredmaterial.Label, theme *decredmaterial.Theme) layout.Widget { + words := strings.Fields(lbl.Text) + if len(words) == 0 { + return func(gtx C) D { return D{} } + } + + return func(gtx C) D { + return layout.Flex{}.Layout(gtx, + layout.Rigid(func(gtx C) D { + lbl.Text = words[0] + if len(words) > 1 { + words = words[1:] + } + return lbl.Layout(gtx) + }), + layout.Flexed(1, func(gtx C) D { + return decredmaterial.GridWrap{ + Axis: layout.Horizontal, + Alignment: layout.Start, + }.Layout(gtx, len(words), func(gtx C, i int) D { + lbl.Text = words[i] + " " + return lbl.Layout(gtx) + }) + }), + ) + } +} + +/** func (r *Renderer) getLabelWeight(weight string) text.Weight { switch weight { case "normal": @@ -58,7 +231,7 @@ func (r *Renderer) getColorFromMap(col string) color.NRGBA { return colorMap["text"] } -func (r *Renderer) styleLabel(label decredmaterial.Label) decredmaterial.Label { +func (r *Renderer) styleHTMLLabel(label decredmaterial.Label) decredmaterial.Label { if len(r.styleGroups) == 0 { return label } @@ -86,7 +259,54 @@ func (r *Renderer) styleLabel(label decredmaterial.Label) decredmaterial.Label { return label } -func (r *Renderer) addStyleGroup(str string) { +func (r *Renderer) setLabelStyle(label decredmaterial.Label, value string) decredmaterial.Label { + if value == "italic" { + label.Font.Style = text.Italic + } + + return label +} + +func (r *Renderer) setLabelWeight(label decredmaterial.Label, value string) decredmaterial.Label { + switch value { + case "bold": + label.Font.Weight = text.Bold + case "normal": + label.Font.Weight = text.Normal + } + + return label +} + +func (r *Renderer) styleMarkdownLabel(label decredmaterial.Label) layout.Widget { + var wdgt layout.Widget + + for i := range r.styleGroups { + for style, value := range r.styleGroups[i] { + switch style { + case "font-style": + wdgt = r.setLabelStyle(label, value).Layout + case "font-weight": + wdgt = r.setLabelWeight(label, value).Layout + case "font-decoration": + if value == strikeTagName { + wdgt = r.strikeLabel(label) + } + } + } + } + + if wdgt == nil { + return label.Layout + } + return wdgt +} + +func (r *Renderer) getMarkdownWidgetAndStyle(label decredmaterial.Label) layout.Widget { + return r.styleMarkdownLabel(label) +} + +func (r *Renderer) addHTMLStyleGroup(str string) { parts := strings.Split(str, "##") styleMap := map[string]string{} @@ -105,8 +325,36 @@ func (r *Renderer) addStyleGroup(str string) { } } +func (r *Renderer) addStyleItem(style, value string) { + styleMap := map[string]string{ + style: value, + } + + r.styleGroups = append(r.styleGroups, styleMap) +} + +func (r *Renderer) addStyleGroupFromTagName(tagName string) { + var key, val string + + switch tagName { + case italicsTagName: + key, val = "font-style", "italic" + case emphTagName: + key, val = "font-style", "italic" + case strongTagName: + key, val = "font-weight", "bold" + case strikeTagName: + key, val = "font-decoration", "strike" + } + + if key != "" && val != "" { + r.addStyleItem(key, val) + } +} + func (r *Renderer) removeLastStyleGroup() { if len(r.styleGroups) > 0 { r.styleGroups = r.styleGroups[:len(r.styleGroups)-1] } } +**/ diff --git a/ui/renderers/table.go b/ui/renderers/table.go index 7a0136298..dde80ecda 100644 --- a/ui/renderers/table.go +++ b/ui/renderers/table.go @@ -48,13 +48,17 @@ func (t *table) startNextRow() { } func (t *table) addCell(content string, alignment cellAlign, isHeader bool) { + if len(t.rows) == 0 { + return + } + cell := cell{ content: content, contentLength: float64(len(content)), alignment: alignment, } - rowIndex := len(t.rows) - 1 + rowIndex := len(t.rows) - 1 t.rows[rowIndex].isHeader = isHeader t.rows[rowIndex].cells = append(t.rows[rowIndex].cells, cell) }