mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-10 18:10:49 +09:00
Merge pull request #242 from anyproto/go-221-block-copy-styled-text
This commit is contained in:
commit
902e758355
4 changed files with 483 additions and 364 deletions
134
core/converter/html/constants.go
Normal file
134
core/converter/html/constants.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package html
|
||||
|
||||
import "github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
|
||||
const (
|
||||
wrapCopyStart = `<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta http-equiv="Content-Style-Type" content="text/css">
|
||||
<title></title>
|
||||
<meta name="Generator" content="Cocoa HTML Writer">
|
||||
<meta name="CocoaVersion" content="1894.1">
|
||||
<style type="text/css">
|
||||
.row > * { display: flex; }
|
||||
.header1 { padding: 23px 0px 1px 0px; font-size: 28px; line-height: 32px; letter-spacing: -0.36px; font-weight: 600; }
|
||||
.header2 { padding: 15px 0px 1px 0px; font-size: 22px; line-height: 28px; letter-spacing: -0.16px; font-weight: 600; }
|
||||
.header3 { padding: 15px 0px 1px 0px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||
.quote { padding: 7px 0px 7px 0px; font-size: 18px; line-height: 26px; font-style: italic; }
|
||||
.paragraph { font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word; }
|
||||
.callout-image { width: 20px; height: 20px; font-size: 16px; line-height: 20px; margin-right: 6px; display: inline-block; }
|
||||
.callout-image img { width: 100%; object-fit: cover; }
|
||||
a { cursor: pointer; }
|
||||
kbd { display: inline; font-family: 'Mono'; line-height: 1.71; background: rgba(247,245,240,0.5); padding: 0px 4px; border-radius: 2px; }
|
||||
ul { margin: 0px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>`
|
||||
wrapCopyEnd = `</body>
|
||||
</html>`
|
||||
wrapExportStart = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title></title>
|
||||
<style type="text/css"></style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="anytype-container">`
|
||||
wrapExportEnd = `</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
styleParagraph = "font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;"
|
||||
styleHeader1 = "padding: 23px 0px 1px 0px; font-size: 28px; line-height: 32px; letter-spacing: -0.36px; font-weight: 600;"
|
||||
styleHeader2 = "padding: 15px 0px 1px 0px; font-size: 22px; line-height: 28px; letter-spacing: -0.16px; font-weight: 600;"
|
||||
styleHeader3 = "padding: 15px 0px 1px 0px; font-size: 17px; line-height: 24px; font-weight: 600;"
|
||||
styleHeader4 = ""
|
||||
styleQuote = "padding: 7px 0px 7px 0px; font-size: 18px; line-height: 26px; font-style: italic;"
|
||||
styleCode = "font-size:15px; font-family: monospace;"
|
||||
styleTitle = ""
|
||||
styleCheckbox = "font-size:15px;"
|
||||
styleToggle = "font-size:15px;"
|
||||
styleKbd = "display: inline; font-family: 'Mono'; line-height: 1.71; background: rgba(247,245,240,0.5); padding: 0px 4px; border-radius: 2px;"
|
||||
styleCallout = "background: #f3f2ec; border-radius: 6px; padding: 16px; margin: 6px 0px;"
|
||||
|
||||
defaultStyle = -1
|
||||
)
|
||||
|
||||
type styleTag struct {
|
||||
OpenTag, CloseTag string
|
||||
}
|
||||
|
||||
var styleTags = map[model.BlockContentTextStyle]styleTag{
|
||||
model.BlockContentText_Header1: {OpenTag: `<h1 style="` + styleHeader1 + `">`, CloseTag: `</h1>`},
|
||||
model.BlockContentText_Header2: {OpenTag: `<h2 style="` + styleHeader2 + `">`, CloseTag: `</h2>`},
|
||||
model.BlockContentText_Header3: {OpenTag: `<h3 style="` + styleHeader3 + `">`, CloseTag: `</h3>`},
|
||||
model.BlockContentText_Header4: {OpenTag: `<h4 style="` + styleHeader4 + `">`, CloseTag: `</h4>`},
|
||||
model.BlockContentText_Quote: {OpenTag: `<quote style="` + styleQuote + `">`, CloseTag: `</quote>`},
|
||||
model.BlockContentText_Code: {OpenTag: `<code style="` + styleCode + `"><pre>`, CloseTag: `</pre></code>`},
|
||||
model.BlockContentText_Title: {OpenTag: `<h1 style="` + styleTitle + `">`, CloseTag: `</h1>`},
|
||||
model.BlockContentText_Checkbox: {OpenTag: `<div style="` + styleCheckbox + `" class="check"><input type="checkbox"/>`, CloseTag: `</div>`},
|
||||
model.BlockContentText_Toggle: {OpenTag: `<div style="` + styleToggle + `" class="toggle">`, CloseTag: `</div>`},
|
||||
defaultStyle: {OpenTag: `<div style="` + styleParagraph + `" class="paragraph" style="` + styleParagraph + `">`, CloseTag: `</div>`},
|
||||
}
|
||||
|
||||
func textColor(color string) string {
|
||||
switch color {
|
||||
case "grey":
|
||||
return "#aca996"
|
||||
case "yellow":
|
||||
return "#ecd91b"
|
||||
case "orange":
|
||||
return "#ffb522"
|
||||
case "red":
|
||||
return "#f55522"
|
||||
case "pink":
|
||||
return "#e51ca0"
|
||||
case "purple":
|
||||
return "#ab50cc"
|
||||
case "blue":
|
||||
return "#3e58"
|
||||
case "ice":
|
||||
return "#2aa7ee"
|
||||
case "teal":
|
||||
return "#0fc8ba"
|
||||
case "lime":
|
||||
return "#5dd400"
|
||||
case "black":
|
||||
return "#2c2b27"
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
func backgroundColor(color string) string {
|
||||
switch color {
|
||||
case "grey":
|
||||
return "#f3f2ec"
|
||||
case "yellow":
|
||||
return "#fef9cc"
|
||||
case "orange":
|
||||
return "#fef3c5"
|
||||
case "red":
|
||||
return "#ffebe5"
|
||||
case "pink":
|
||||
return "#fee3f5"
|
||||
case "purple":
|
||||
return "#f4e3fa"
|
||||
case "blue":
|
||||
return "#f4e3fa"
|
||||
case "ice":
|
||||
return "#d6effd"
|
||||
case "teal":
|
||||
return "#d6f5f3"
|
||||
case "lime":
|
||||
return "#e3f7d0"
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
|
@ -18,49 +18,6 @@ import (
|
|||
utf16 "github.com/anyproto/anytype-heart/util/text"
|
||||
)
|
||||
|
||||
const (
|
||||
wrapCopyStart = `<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta http-equiv="Content-Style-Type" content="text/css">
|
||||
<title></title>
|
||||
<meta name="Generator" content="Cocoa HTML Writer">
|
||||
<meta name="CocoaVersion" content="1894.1">
|
||||
<style type="text/css">
|
||||
.row > * { display: flex; }
|
||||
.header1 { padding: 23px 0px 1px 0px; font-size: 28px; line-height: 32px; letter-spacing: -0.36px; font-weight: 600; }
|
||||
.header2 { padding: 15px 0px 1px 0px; font-size: 22px; line-height: 28px; letter-spacing: -0.16px; font-weight: 600; }
|
||||
.header3 { padding: 15px 0px 1px 0px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||
.quote { padding: 7px 0px 7px 0px; font-size: 18px; line-height: 26px; font-style: italic; }
|
||||
.paragraph { font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word; }
|
||||
.callout-image { width: 20px; height: 20px; font-size: 16px; line-height: 20px; margin-right: 6px; display: inline-block; }
|
||||
.callout-image img { width: 100%; object-fit: cover; }
|
||||
a { cursor: pointer; }
|
||||
kbd { display: inline; font-family: 'Mono'; line-height: 1.71; background: rgba(247,245,240,0.5); padding: 0px 4px; border-radius: 2px; }
|
||||
ul { margin: 0px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>`
|
||||
wrapCopyEnd = `</body>
|
||||
</html>`
|
||||
wrapExportStart = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title></title>
|
||||
<style type="text/css"></style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="anytype-container">`
|
||||
wrapExportEnd = `</div>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
func NewHTMLConverter(fileService files.Service, s *state.State) *HTML {
|
||||
return &HTML{fileService: fileService, s: s}
|
||||
}
|
||||
|
@ -140,159 +97,14 @@ func (h *HTML) renderChildren(parent *model.Block) {
|
|||
}
|
||||
|
||||
func (h *HTML) renderText(rs *renderState, b *model.Block) {
|
||||
styleParagraph := "font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;"
|
||||
styleHeader1 := "padding: 23px 0px 1px 0px; font-size: 28px; line-height: 32px; letter-spacing: -0.36px; font-weight: 600;"
|
||||
styleHeader2 := "padding: 15px 0px 1px 0px; font-size: 22px; line-height: 28px; letter-spacing: -0.16px; font-weight: 600;"
|
||||
styleHeader3 := "padding: 15px 0px 1px 0px; font-size: 17px; line-height: 24px; font-weight: 600;"
|
||||
styleHeader4 := ""
|
||||
styleQuote := "padding: 7px 0px 7px 0px; font-size: 18px; line-height: 26px; font-style: italic;"
|
||||
styleCode := "font-size:15px; font-family: monospace;"
|
||||
styleTitle := ""
|
||||
styleCheckbox := "font-size:15px;"
|
||||
styleToggle := "font-size:15px;"
|
||||
styleKbd := "display: inline; font-family: 'Mono'; line-height: 1.71; background: rgba(247,245,240,0.5); padding: 0px 4px; border-radius: 2px;"
|
||||
styleCallout := "background: #f3f2ec; border-radius: 6px; padding: 16px; margin: 6px 0px;"
|
||||
|
||||
text := b.GetText()
|
||||
|
||||
writeMark := func(m *model.BlockContentTextMark, start bool) {
|
||||
switch m.Type {
|
||||
case model.BlockContentTextMark_Strikethrough:
|
||||
if start {
|
||||
h.buf.WriteString("<s>")
|
||||
} else {
|
||||
h.buf.WriteString("</s>")
|
||||
}
|
||||
case model.BlockContentTextMark_Keyboard:
|
||||
if start {
|
||||
h.buf.WriteString(`<kbd style="` + styleKbd + `">`)
|
||||
} else {
|
||||
h.buf.WriteString(`</kbd>`)
|
||||
}
|
||||
case model.BlockContentTextMark_Italic:
|
||||
if start {
|
||||
h.buf.WriteString("<i>")
|
||||
} else {
|
||||
h.buf.WriteString("</i>")
|
||||
}
|
||||
case model.BlockContentTextMark_Bold:
|
||||
if start {
|
||||
h.buf.WriteString("<b>")
|
||||
} else {
|
||||
h.buf.WriteString("</b>")
|
||||
}
|
||||
case model.BlockContentTextMark_Link:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<a href="%s">`, m.Param)
|
||||
} else {
|
||||
h.buf.WriteString("</a>")
|
||||
}
|
||||
case model.BlockContentTextMark_TextColor:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<span style="color:%s">`, textColor(m.Param))
|
||||
} else {
|
||||
h.buf.WriteString("</span>")
|
||||
}
|
||||
case model.BlockContentTextMark_BackgroundColor:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<span style="backgound-color:%s">`, backgroundColor(m.Param))
|
||||
} else {
|
||||
h.buf.WriteString("</span>")
|
||||
}
|
||||
case model.BlockContentTextMark_Underscored:
|
||||
if start {
|
||||
h.buf.WriteString("<u>")
|
||||
} else {
|
||||
h.buf.WriteString("</u>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderText := func() {
|
||||
var breakpoints = make(map[int]struct{})
|
||||
if text.Marks != nil {
|
||||
for _, m := range text.Marks.Marks {
|
||||
breakpoints[int(m.Range.From)] = struct{}{}
|
||||
breakpoints[int(m.Range.To)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
textLen := utf16.UTF16RuneCountString(text.Text)
|
||||
runes := []rune(text.Text)
|
||||
// the end position of markdown text equals full length of text
|
||||
for i := 0; i <= textLen; i++ {
|
||||
if _, ok := breakpoints[i]; ok {
|
||||
for _, m := range text.Marks.Marks {
|
||||
if int(m.Range.To) == i {
|
||||
writeMark(m, false)
|
||||
}
|
||||
// i == textLen
|
||||
if int(m.Range.From) == i {
|
||||
writeMark(m, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i < len(runes) {
|
||||
h.buf.WriteString(html.EscapeString(string(runes[i])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch text.Style {
|
||||
case model.BlockContentText_Header1:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<h1 style="` + styleHeader1 + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</h1>`)
|
||||
case model.BlockContentText_Header2:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<h2 style="` + styleHeader2 + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</h2>`)
|
||||
case model.BlockContentText_Header3:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<h3 style="` + styleHeader3 + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</h3>`)
|
||||
case model.BlockContentText_Header4:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<h4 style="` + styleHeader4 + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</h4>`)
|
||||
case model.BlockContentText_Quote:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<quote style="` + styleQuote + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</quote>`)
|
||||
case model.BlockContentText_Code:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<code style="` + styleCode + `"><pre>`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</pre></code>`)
|
||||
case model.BlockContentText_Title:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<h1 style="` + styleTitle + `">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</h1>`)
|
||||
case model.BlockContentText_Checkbox:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<div style="` + styleCheckbox + `" class="check"><input type="checkbox"/>`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</div>`)
|
||||
case model.BlockContentText_Marked:
|
||||
if rs.isFirst {
|
||||
rs.OpenUL()
|
||||
}
|
||||
h.buf.WriteString(`<li>`)
|
||||
renderText()
|
||||
h.writeTextToBuf(text)
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</li>`)
|
||||
if rs.isLast {
|
||||
|
@ -303,18 +115,12 @@ func (h *HTML) renderText(rs *renderState, b *model.Block) {
|
|||
rs.OpenOL()
|
||||
}
|
||||
h.buf.WriteString(`<li>`)
|
||||
renderText()
|
||||
h.writeTextToBuf(text)
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</li>`)
|
||||
if rs.isLast {
|
||||
rs.Close()
|
||||
}
|
||||
case model.BlockContentText_Toggle:
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<div style="` + styleToggle + `" class="toggle">`)
|
||||
renderText()
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</div>`)
|
||||
case model.BlockContentText_Callout:
|
||||
rs.Close()
|
||||
|
||||
|
@ -324,15 +130,19 @@ func (h *HTML) renderText(rs *renderState, b *model.Block) {
|
|||
}
|
||||
|
||||
fmt.Fprintf(h.buf, `<div style="%s">%s`, styleCallout, img)
|
||||
renderText()
|
||||
h.writeTextToBuf(text)
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</div>`)
|
||||
default:
|
||||
tags, ok := styleTags[text.Style]
|
||||
if !ok {
|
||||
tags = styleTags[defaultStyle]
|
||||
}
|
||||
rs.Close()
|
||||
h.buf.WriteString(`<div style="` + styleParagraph + `" class="paragraph" style="` + styleParagraph + `">`)
|
||||
renderText()
|
||||
h.buf.WriteString(tags.OpenTag)
|
||||
h.writeTextToBuf(text)
|
||||
h.renderChildren(b)
|
||||
h.buf.WriteString(`</div>`)
|
||||
h.buf.WriteString(tags.CloseTag)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -524,6 +334,130 @@ func (h *HTML) renderCell(colWidth map[string]float64, colId string, colToCell m
|
|||
}
|
||||
}
|
||||
|
||||
func (h *HTML) writeTag(m *model.BlockContentTextMark, start bool) {
|
||||
switch m.Type {
|
||||
case model.BlockContentTextMark_Strikethrough:
|
||||
if start {
|
||||
h.buf.WriteString("<s>")
|
||||
} else {
|
||||
h.buf.WriteString("</s>")
|
||||
}
|
||||
case model.BlockContentTextMark_Keyboard:
|
||||
if start {
|
||||
h.buf.WriteString(`<kbd style="` + styleKbd + `">`)
|
||||
} else {
|
||||
h.buf.WriteString(`</kbd>`)
|
||||
}
|
||||
case model.BlockContentTextMark_Italic:
|
||||
if start {
|
||||
h.buf.WriteString("<i>")
|
||||
} else {
|
||||
h.buf.WriteString("</i>")
|
||||
}
|
||||
case model.BlockContentTextMark_Bold:
|
||||
if start {
|
||||
h.buf.WriteString("<b>")
|
||||
} else {
|
||||
h.buf.WriteString("</b>")
|
||||
}
|
||||
case model.BlockContentTextMark_Link:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<a href="%s">`, m.Param)
|
||||
} else {
|
||||
h.buf.WriteString("</a>")
|
||||
}
|
||||
case model.BlockContentTextMark_TextColor:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<span style="color:%s">`, textColor(m.Param))
|
||||
} else {
|
||||
h.buf.WriteString("</span>")
|
||||
}
|
||||
case model.BlockContentTextMark_BackgroundColor:
|
||||
if start {
|
||||
fmt.Fprintf(h.buf, `<span style="backgound-color:%s">`, backgroundColor(m.Param))
|
||||
} else {
|
||||
h.buf.WriteString("</span>")
|
||||
}
|
||||
case model.BlockContentTextMark_Underscored:
|
||||
if start {
|
||||
h.buf.WriteString("<u>")
|
||||
} else {
|
||||
h.buf.WriteString("</u>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTML) closeTagsUntil(
|
||||
text *model.BlockContentText,
|
||||
lastOpenedTags *[]model.BlockContentTextMarkType,
|
||||
bottom model.BlockContentTextMarkType,
|
||||
index int,
|
||||
) {
|
||||
closed := 0
|
||||
for _, tag := range *lastOpenedTags {
|
||||
if tag == bottom {
|
||||
*lastOpenedTags = (*lastOpenedTags)[closed:]
|
||||
return
|
||||
}
|
||||
for _, mark := range text.Marks.Marks {
|
||||
if mark.Type == tag && int(mark.Range.From) < index && int(mark.Range.To) >= index {
|
||||
h.writeTag(mark, false)
|
||||
if int(mark.Range.To) == index {
|
||||
mark.Range.To--
|
||||
} else {
|
||||
mark.Range.From = int32(index)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
closed++
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTML) writeTextToBuf(text *model.BlockContentText) {
|
||||
var (
|
||||
breakpoints = make(map[int]struct{})
|
||||
lastOpenedTags = make([]model.BlockContentTextMarkType, 0)
|
||||
)
|
||||
if text.Marks != nil {
|
||||
for _, m := range text.Marks.Marks {
|
||||
breakpoints[int(m.Range.From)] = struct{}{}
|
||||
breakpoints[int(m.Range.To)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
textLen := utf16.UTF16RuneCountString(text.Text)
|
||||
runes := []rune(text.Text)
|
||||
// the end position of markdown text equals full length of text
|
||||
for i := 0; i <= textLen; i++ {
|
||||
if _, ok := breakpoints[i]; ok {
|
||||
// iterate marks forwards to put closing tags
|
||||
for _, m := range text.Marks.Marks {
|
||||
if int(m.Range.To) == i {
|
||||
//TODO: check lastOpenedTags on zero length ?
|
||||
if lastOpenedTags[0] != m.Type {
|
||||
h.closeTagsUntil(text, &lastOpenedTags, m.Type, i)
|
||||
}
|
||||
h.writeTag(m, false)
|
||||
lastOpenedTags = lastOpenedTags[1:]
|
||||
}
|
||||
}
|
||||
// iterate marks backwards to put opening tags
|
||||
for j := len(text.Marks.Marks) - 1; j >= 0; j-- {
|
||||
m := text.Marks.Marks[j]
|
||||
if int(m.Range.From) == i {
|
||||
h.writeTag(m, true)
|
||||
lastOpenedTags = append([]model.BlockContentTextMarkType{m.Type}, lastOpenedTags...)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if i < len(runes) {
|
||||
h.buf.WriteString(html.EscapeString(string(runes[i])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTML) getImageBase64(hash string) (res string) {
|
||||
im, err := h.fileService.ImageByHash(context.TODO(), hash)
|
||||
if err != nil {
|
||||
|
@ -541,98 +475,3 @@ func (h *HTML) getImageBase64(hash string) (res string) {
|
|||
dataBase64 := base64.StdEncoding.EncodeToString(data)
|
||||
return fmt.Sprintf("data:%s;base64, %s", f.Meta().Media, dataBase64)
|
||||
}
|
||||
|
||||
type renderState struct {
|
||||
ulOpened, olOpened bool
|
||||
isFirst, isLast bool
|
||||
|
||||
h *HTML
|
||||
}
|
||||
|
||||
func (rs *renderState) OpenUL() {
|
||||
if rs.ulOpened {
|
||||
return
|
||||
}
|
||||
if rs.olOpened {
|
||||
rs.Close()
|
||||
}
|
||||
rs.h.buf.WriteString(`<ul style="font-size:15px;">`)
|
||||
rs.ulOpened = true
|
||||
}
|
||||
|
||||
func (rs *renderState) OpenOL() {
|
||||
if rs.olOpened {
|
||||
return
|
||||
}
|
||||
if rs.ulOpened {
|
||||
rs.Close()
|
||||
}
|
||||
rs.h.buf.WriteString("<ol style=\"font-size:15px;\">")
|
||||
rs.olOpened = true
|
||||
}
|
||||
|
||||
func (rs *renderState) Close() {
|
||||
if rs.ulOpened {
|
||||
rs.h.buf.WriteString("</ul>")
|
||||
rs.ulOpened = false
|
||||
} else if rs.olOpened {
|
||||
rs.h.buf.WriteString("</ol>")
|
||||
rs.olOpened = false
|
||||
}
|
||||
}
|
||||
|
||||
func textColor(color string) string {
|
||||
switch color {
|
||||
case "grey":
|
||||
return "#aca996"
|
||||
case "yellow":
|
||||
return "#ecd91b"
|
||||
case "orange":
|
||||
return "#ffb522"
|
||||
case "red":
|
||||
return "#f55522"
|
||||
case "pink":
|
||||
return "#e51ca0"
|
||||
case "purple":
|
||||
return "#ab50cc"
|
||||
case "blue":
|
||||
return "#3e58"
|
||||
case "ice":
|
||||
return "#2aa7ee"
|
||||
case "teal":
|
||||
return "#0fc8ba"
|
||||
case "lime":
|
||||
return "#5dd400"
|
||||
case "black":
|
||||
return "#2c2b27"
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
func backgroundColor(color string) string {
|
||||
switch color {
|
||||
case "grey":
|
||||
return "#f3f2ec"
|
||||
case "yellow":
|
||||
return "#fef9cc"
|
||||
case "orange":
|
||||
return "#fef3c5"
|
||||
case "red":
|
||||
return "#ffebe5"
|
||||
case "pink":
|
||||
return "#fee3f5"
|
||||
case "purple":
|
||||
return "#f4e3fa"
|
||||
case "blue":
|
||||
return "#f4e3fa"
|
||||
case "ice":
|
||||
return "#d6effd"
|
||||
case "teal":
|
||||
return "#d6f5f3"
|
||||
case "lime":
|
||||
return "#e3f7d0"
|
||||
default:
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/simple"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTML_Convert(t *testing.T) {
|
||||
|
@ -22,48 +22,16 @@ func TestHTML_Convert(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("markup", func(t *testing.T) {
|
||||
s := state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{ChildrenIds: []string{"1"}}),
|
||||
"1": simple.New(&model.Block{
|
||||
Id: "1",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "0123456789",
|
||||
Marks: &model.BlockContentTextMarks{
|
||||
Marks: []*model.BlockContentTextMark{
|
||||
{
|
||||
Range: &model.Range{To: 2},
|
||||
Type: model.BlockContentTextMark_Bold,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 1, To: 2},
|
||||
Type: model.BlockContentTextMark_Italic,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 2, To: 3},
|
||||
Type: model.BlockContentTextMark_Link,
|
||||
Param: "http://test.test",
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 3, To: 4},
|
||||
Type: model.BlockContentTextMark_TextColor,
|
||||
Param: "grey",
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 3, To: 4},
|
||||
Type: model.BlockContentTextMark_Underscored,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).(*state.State)
|
||||
res := NewHTMLConverter(nil, s).Convert()
|
||||
res = strings.ReplaceAll(res, wrapCopyStart, "")
|
||||
res = strings.ReplaceAll(res, wrapCopyEnd, "")
|
||||
exp := `<div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;"><b>0<i>1</b></i><a href="http://test.test">2</a><span style="color:#aca996"><u>3</span></u>456789</div>`
|
||||
assert.Equal(t, exp, res)
|
||||
//given
|
||||
doc := givenMarkup()
|
||||
|
||||
//when
|
||||
html := convertHtml(doc)
|
||||
|
||||
//then
|
||||
expected := givenTrimmedString(markupExpectation)
|
||||
|
||||
assert.Equal(t, expected, givenTrimmedString(html))
|
||||
})
|
||||
|
||||
t.Run("lists", func(t *testing.T) {
|
||||
|
@ -92,32 +60,30 @@ func TestHTML_Convert(t *testing.T) {
|
|||
assert.Equal(t, expected, givenTrimmedString(html))
|
||||
})
|
||||
|
||||
t.Run("intersection of marks", func(t *testing.T) {
|
||||
//given
|
||||
doc := givenIntersectMarks()
|
||||
|
||||
//when
|
||||
html := convertHtml(doc)
|
||||
|
||||
//then
|
||||
expected := givenTrimmedString(intersectMarksExpectation)
|
||||
|
||||
assert.Equal(t, expected, givenTrimmedString(html))
|
||||
})
|
||||
|
||||
t.Run("columns", func(t *testing.T) {
|
||||
s := state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{Id: "root", ChildrenIds: []string{"1"}}),
|
||||
}).(*state.State)
|
||||
s.Set(simple.New(&model.Block{
|
||||
Id: "1",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "1",
|
||||
},
|
||||
},
|
||||
}))
|
||||
s.Set(simple.New(&model.Block{
|
||||
Id: "2",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "2",
|
||||
},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, s.InsertTo("1", model.Block_Right, "2"))
|
||||
res := NewHTMLConverter(nil, s).Convert()
|
||||
res = strings.ReplaceAll(res, wrapCopyStart, "")
|
||||
res = strings.ReplaceAll(res, wrapCopyEnd, "")
|
||||
exp := `<div class="row" style="display: flex"><div class="column" ><div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;">1</div></div><div class="column" ><div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;">2</div></div></div>`
|
||||
assert.Equal(t, exp, res)
|
||||
//given
|
||||
doc := givenColumns()
|
||||
|
||||
//when
|
||||
html := convertHtml(doc)
|
||||
|
||||
//then
|
||||
expected := givenTrimmedString(columnsExpectation)
|
||||
|
||||
assert.Equal(t, expected, givenTrimmedString(html))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -125,6 +91,46 @@ func convertHtml(s *state.State) string {
|
|||
return NewHTMLConverter(nil, s).Convert()
|
||||
}
|
||||
|
||||
func givenMarkup() *state.State {
|
||||
return state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{ChildrenIds: []string{"1"}}),
|
||||
"1": simple.New(&model.Block{
|
||||
Id: "1",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "0123456789",
|
||||
Marks: &model.BlockContentTextMarks{
|
||||
Marks: []*model.BlockContentTextMark{
|
||||
{
|
||||
Range: &model.Range{To: 2},
|
||||
Type: model.BlockContentTextMark_Bold,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 1, To: 2},
|
||||
Type: model.BlockContentTextMark_Italic,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 2, To: 3},
|
||||
Type: model.BlockContentTextMark_Link,
|
||||
Param: "http://test.test",
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 3, To: 4},
|
||||
Type: model.BlockContentTextMark_TextColor,
|
||||
Param: "grey",
|
||||
},
|
||||
{
|
||||
Range: &model.Range{From: 3, To: 4},
|
||||
Type: model.BlockContentTextMark_Underscored,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).(*state.State)
|
||||
}
|
||||
|
||||
func givenLists() *state.State {
|
||||
s := state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{ChildrenIds: []string{"1", "2", "3", "4", "5", "6"}}),
|
||||
|
@ -287,6 +293,71 @@ func givenListsInLists() *state.State {
|
|||
return s
|
||||
}
|
||||
|
||||
func givenIntersectMarks() *state.State {
|
||||
s := state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{ChildrenIds: []string{"1"}}),
|
||||
}).(*state.State)
|
||||
s.Add(simple.New(&model.Block{
|
||||
Id: "1",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "abcdef",
|
||||
Style: model.BlockContentText_Marked,
|
||||
Marks: &model.BlockContentTextMarks{
|
||||
Marks: []*model.BlockContentTextMark{
|
||||
{
|
||||
Range: &model.Range{
|
||||
From: 4,
|
||||
To: 5,
|
||||
},
|
||||
Type: model.BlockContentTextMark_Italic,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{
|
||||
From: 2,
|
||||
To: 6,
|
||||
},
|
||||
Type: model.BlockContentTextMark_Bold,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{
|
||||
From: 1,
|
||||
To: 3,
|
||||
},
|
||||
Type: model.BlockContentTextMark_Italic,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
return s
|
||||
}
|
||||
|
||||
func givenColumns() *state.State {
|
||||
s := state.NewDoc("root", map[string]simple.Block{
|
||||
"root": simple.New(&model.Block{Id: "root", ChildrenIds: []string{"1"}}),
|
||||
}).(*state.State)
|
||||
s.Set(simple.New(&model.Block{
|
||||
Id: "1",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "1",
|
||||
},
|
||||
},
|
||||
}))
|
||||
s.Set(simple.New(&model.Block{
|
||||
Id: "2",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "2",
|
||||
},
|
||||
},
|
||||
}))
|
||||
_ = s.InsertTo("1", model.Block_Right, "2")
|
||||
return s
|
||||
}
|
||||
|
||||
func givenTrimmedString(s string) string {
|
||||
s = strings.ReplaceAll(s, wrapCopyStart, "")
|
||||
s = strings.ReplaceAll(s, wrapCopyEnd, "")
|
||||
|
@ -294,6 +365,18 @@ func givenTrimmedString(s string) string {
|
|||
return res
|
||||
}
|
||||
|
||||
const markupExpectation = `
|
||||
<div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;">
|
||||
<b>0
|
||||
<i>1</i>
|
||||
</b>
|
||||
<a href="http://test.test">2</a>
|
||||
<u>
|
||||
<span style="color:#aca996">3</span>
|
||||
</u>456789
|
||||
</div>
|
||||
`
|
||||
|
||||
const listExpectation = `
|
||||
<ol style=\"font-size:15px;\">
|
||||
<li>1</li>
|
||||
|
@ -335,3 +418,26 @@ const listInListExpectation = `
|
|||
</ul>
|
||||
</li>
|
||||
</ol>`
|
||||
|
||||
const intersectMarksExpectation = `
|
||||
<ul style="font-size:15px;">
|
||||
<li>a
|
||||
<i>b
|
||||
<b>c</b>
|
||||
</i>
|
||||
<b>d
|
||||
<i>e</i>f
|
||||
</b>
|
||||
</li>
|
||||
</ul>
|
||||
`
|
||||
|
||||
const columnsExpectation = `
|
||||
<div class="row" style="display: flex">
|
||||
<div class="column" >
|
||||
<div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;">1</div></div><div class="column" ><div style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;" class="paragraph" style="font-size: 15px; line-height: 24px; letter-spacing: -0.08px; font-weight: 400; word-wrap: break-word;">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
40
core/converter/html/renderstate.go
Normal file
40
core/converter/html/renderstate.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package html
|
||||
|
||||
type renderState struct {
|
||||
ulOpened, olOpened bool
|
||||
isFirst, isLast bool
|
||||
|
||||
h *HTML
|
||||
}
|
||||
|
||||
func (rs *renderState) OpenUL() {
|
||||
if rs.ulOpened {
|
||||
return
|
||||
}
|
||||
if rs.olOpened {
|
||||
rs.Close()
|
||||
}
|
||||
rs.h.buf.WriteString(`<ul style="font-size:15px;">`)
|
||||
rs.ulOpened = true
|
||||
}
|
||||
|
||||
func (rs *renderState) OpenOL() {
|
||||
if rs.olOpened {
|
||||
return
|
||||
}
|
||||
if rs.ulOpened {
|
||||
rs.Close()
|
||||
}
|
||||
rs.h.buf.WriteString("<ol style=\"font-size:15px;\">")
|
||||
rs.olOpened = true
|
||||
}
|
||||
|
||||
func (rs *renderState) Close() {
|
||||
if rs.ulOpened {
|
||||
rs.h.buf.WriteString("</ul>")
|
||||
rs.ulOpened = false
|
||||
} else if rs.olOpened {
|
||||
rs.h.buf.WriteString("</ol>")
|
||||
rs.olOpened = false
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue