1
0
Fork 0
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:
Kirill Stonozhenko 2023-08-02 11:10:35 +02:00 committed by GitHub
commit 902e758355
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 483 additions and 364 deletions

View 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
}
}

View file

@ -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
}
}

View file

@ -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>
`

View 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
}
}