Use prosemirror pkg to render document PDF
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
2
go.mod
2
go.mod
@@ -32,8 +32,6 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vektah/gqlparser/v2 v2.5.32
|
||||
github.com/vikstrous/dataloadgen v0.0.10
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
go.abhg.dev/goldmark/mermaid v0.6.0
|
||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712
|
||||
go.gearno.de/kit v0.1.1
|
||||
go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c
|
||||
|
||||
4
go.sum
4
go.sum
@@ -322,10 +322,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.abhg.dev/goldmark/mermaid v0.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY=
|
||||
go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I=
|
||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712 h1:J5ccbcxFuwxe6Oa9fVi9FqQOo+n17ni4wbl9t4NuEzc=
|
||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712/go.mod h1:fnIIvKO9QnsyLO3ZJLJT3r8KZv/p0FOeT5eZKilYWXg=
|
||||
go.gearno.de/kit v0.1.1 h1:QuBZCZ/h2Eyh6DjjR6CGjkdsab/ztHz6xUiIk0FeREE=
|
||||
|
||||
@@ -23,11 +23,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||
"go.abhg.dev/goldmark/mermaid"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/prosemirror"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -80,26 +77,6 @@ var (
|
||||
}
|
||||
return "No"
|
||||
},
|
||||
"formatContent": func(content string) template.HTML {
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.Table,
|
||||
&mermaid.Extender{
|
||||
RenderMode: mermaid.RenderModeClient,
|
||||
NoScript: true,
|
||||
},
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
gmhtml.WithUnsafe(),
|
||||
),
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(content), &buf); err != nil {
|
||||
return template.HTML(fmt.Sprintf("<p>%s</p>", html.EscapeString(content)))
|
||||
}
|
||||
return template.HTML(buf.String())
|
||||
},
|
||||
"imgTag": func(src, alt, class string) template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" class="%s">`, html.EscapeString(src), html.EscapeString(alt), html.EscapeString(class)))
|
||||
},
|
||||
@@ -217,7 +194,7 @@ type (
|
||||
|
||||
DocumentData struct {
|
||||
Title string
|
||||
Content string
|
||||
Content string // ProseMirror/Tiptap document JSON; use ProseMirrorJSONToHTML for HTML
|
||||
Major int
|
||||
Minor int
|
||||
Classification Classification
|
||||
@@ -340,11 +317,37 @@ const (
|
||||
ClassificationSecret Classification = "SECRET"
|
||||
)
|
||||
|
||||
// ProseMirrorJSONToHTML converts ProseMirror/Tiptap document JSON to an HTML fragment.
|
||||
// On parse or render failure it returns a single escaped paragraph with the raw input.
|
||||
func ProseMirrorJSONToHTML(content string) template.HTML {
|
||||
s := strings.TrimSpace(content)
|
||||
if s == "" {
|
||||
return template.HTML("")
|
||||
}
|
||||
node, err := prosemirror.Parse(s)
|
||||
if err != nil {
|
||||
return template.HTML(fmt.Sprintf("<p>%s</p>", html.EscapeString(s)))
|
||||
}
|
||||
htmlStr, err := prosemirror.RenderHTML(node)
|
||||
if err != nil {
|
||||
return template.HTML(fmt.Sprintf("<p>%s</p>", html.EscapeString(s)))
|
||||
}
|
||||
return template.HTML(htmlStr)
|
||||
}
|
||||
|
||||
func RenderHTML(data DocumentData) ([]byte, error) {
|
||||
data.MermaidJS = template.JS(mermaidJSSource)
|
||||
|
||||
page := struct {
|
||||
DocumentData
|
||||
BodyHTML template.HTML
|
||||
}{
|
||||
DocumentData: data,
|
||||
BodyHTML: ProseMirrorJSONToHTML(data.Content),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := documentTemplate.Execute(&buf, data); err != nil {
|
||||
if err := documentTemplate.Execute(&buf, page); err != nil {
|
||||
return nil, fmt.Errorf("cannot execute template: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestRenderHTML(t *testing.T) {
|
||||
name: "basic document with all fields",
|
||||
data: DocumentData{
|
||||
Title: "Test Document",
|
||||
Content: "# Main Title\n\nThis is **bold** text with *italic* formatting.",
|
||||
Content: `{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Main Title"}]},{"type":"paragraph","content":[{"type":"text","text":"This is "},{"type":"text","marks":[{"type":"bold"}],"text":"bold"},{"type":"text","text":" text with "},{"type":"text","marks":[{"type":"italic"}],"text":"italic"},{"type":"text","text":" formatting."}]}]}`,
|
||||
Major: 1,
|
||||
Classification: ClassificationPublic,
|
||||
Approvers: []string{"John Doe"},
|
||||
@@ -87,20 +87,32 @@ func TestRenderHTML(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "document with markdown content",
|
||||
name: "document with prosemirror content",
|
||||
data: DocumentData{
|
||||
Title: "Markdown Test",
|
||||
Content: "## Section 1\n\n- Item 1\n- Item 2\n\n**Bold text** and *italic text*\n\n```code block```",
|
||||
Title: "ProseMirror Test",
|
||||
Content: `{"type":"doc","content":[` +
|
||||
`{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Section 1"}]},` +
|
||||
`{"type":"bulletList","content":[` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 1"}]}]},` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 2"}]}]}` +
|
||||
`]},` +
|
||||
`{"type":"paragraph","content":[` +
|
||||
`{"type":"text","marks":[{"type":"bold"}],"text":"Bold text"},` +
|
||||
`{"type":"text","text":" and "},` +
|
||||
`{"type":"text","marks":[{"type":"italic"}],"text":"italic text"}` +
|
||||
`]},` +
|
||||
`{"type":"codeBlock","content":[{"type":"text","text":"code block"}]}` +
|
||||
`]}`,
|
||||
},
|
||||
wantContains: []string{
|
||||
"<h2>Section 1</h2>",
|
||||
"<ul>",
|
||||
"<li>Item 1</li>",
|
||||
"<li>Item 2</li>",
|
||||
"<li><p>Item 1</p></li>",
|
||||
"<li><p>Item 2</p></li>",
|
||||
"</ul>",
|
||||
"<strong>Bold text</strong>",
|
||||
"<em>italic text</em>",
|
||||
"<code>code block</code>",
|
||||
"<pre><code>code block</code></pre>",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -176,10 +188,9 @@ func TestRenderHTML(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderHTML_ErrorHandling(t *testing.T) {
|
||||
// Test with data that should not cause errors
|
||||
data := DocumentData{
|
||||
Title: "Valid Document",
|
||||
Content: "Valid content",
|
||||
Content: `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Valid content"}]}]}`,
|
||||
}
|
||||
|
||||
result, err := RenderHTML(data)
|
||||
@@ -214,20 +225,21 @@ func TestTemplateFunctions(t *testing.T) {
|
||||
assert.Equal(t, "CONFIDENTIAL", classFunc(ClassificationConfidential))
|
||||
})
|
||||
|
||||
t.Run("formatContent function", func(t *testing.T) {
|
||||
formatFunc := templateFuncs["formatContent"].(func(string) template.HTML)
|
||||
|
||||
// Test markdown conversion
|
||||
result := formatFunc("**bold** text")
|
||||
t.Run("ProseMirrorJSONToHTML", func(t *testing.T) {
|
||||
result := ProseMirrorJSONToHTML(
|
||||
`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"plain "},{"type":"text","marks":[{"type":"bold"}],"text":"bold"}]}]}`,
|
||||
)
|
||||
assert.Contains(t, string(result), "<strong>bold</strong>")
|
||||
|
||||
// Test basic text
|
||||
result = formatFunc("simple text")
|
||||
result = ProseMirrorJSONToHTML(
|
||||
`{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"simple text"}]}]}`,
|
||||
)
|
||||
assert.Contains(t, string(result), "<p>simple text</p>")
|
||||
|
||||
// Test empty content - goldmark produces empty output for empty input
|
||||
result = formatFunc("")
|
||||
// Empty content should produce empty result from goldmark
|
||||
result = ProseMirrorJSONToHTML("**not** json")
|
||||
assert.Contains(t, string(result), "<p>**not** json</p>")
|
||||
|
||||
result = ProseMirrorJSONToHTML("")
|
||||
assert.Equal(t, template.HTML(""), result)
|
||||
})
|
||||
}
|
||||
@@ -276,44 +288,62 @@ func TestHTMLEscaping(t *testing.T) {
|
||||
assert.Contains(t, resultStr, "'")
|
||||
}
|
||||
|
||||
func TestMarkdownRendering(t *testing.T) {
|
||||
func TestProseMirrorContentRendering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown string
|
||||
want []string
|
||||
name string
|
||||
content string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "headers",
|
||||
markdown: "# H1\n## H2\n### H3",
|
||||
want: []string{"<h1>H1</h1>", "<h2>H2</h2>", "<h3>H3</h3>"},
|
||||
name: "headers",
|
||||
content: `{"type":"doc","content":[` +
|
||||
`{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"H1"}]},` +
|
||||
`{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"H2"}]},` +
|
||||
`{"type":"heading","attrs":{"level":3},"content":[{"type":"text","text":"H3"}]}` +
|
||||
`]}`,
|
||||
want: []string{"<h1>H1</h1>", "<h2>H2</h2>", "<h3>H3</h3>"},
|
||||
},
|
||||
{
|
||||
name: "emphasis",
|
||||
markdown: "**bold** and *italic*",
|
||||
want: []string{"<strong>bold</strong>", "<em>italic</em>"},
|
||||
name: "emphasis",
|
||||
content: `{"type":"doc","content":[{"type":"paragraph","content":[` +
|
||||
`{"type":"text","marks":[{"type":"bold"}],"text":"bold"},` +
|
||||
`{"type":"text","text":" and "},` +
|
||||
`{"type":"text","marks":[{"type":"italic"}],"text":"italic"}` +
|
||||
`]}]}`,
|
||||
want: []string{"<strong>bold</strong>", "<em>italic</em>"},
|
||||
},
|
||||
{
|
||||
name: "lists",
|
||||
markdown: "- Item 1\n- Item 2",
|
||||
want: []string{"<ul>", "<li>Item 1</li>", "<li>Item 2</li>", "</ul>"},
|
||||
name: "lists",
|
||||
content: `{"type":"doc","content":[{"type":"bulletList","content":[` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 1"}]}]},` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 2"}]}]}` +
|
||||
`]}]}`,
|
||||
want: []string{"<ul>", "<li><p>Item 1</p></li>", "<li><p>Item 2</p></li>", "</ul>"},
|
||||
},
|
||||
{
|
||||
name: "paragraphs",
|
||||
markdown: "Paragraph 1\n\nParagraph 2",
|
||||
want: []string{"<p>Paragraph 1</p>", "<p>Paragraph 2</p>"},
|
||||
name: "paragraphs",
|
||||
content: `{"type":"doc","content":[` +
|
||||
`{"type":"paragraph","content":[{"type":"text","text":"Paragraph 1"}]},` +
|
||||
`{"type":"paragraph","content":[{"type":"text","text":"Paragraph 2"}]}` +
|
||||
`]}`,
|
||||
want: []string{"<p>Paragraph 1</p>", "<p>Paragraph 2</p>"},
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
markdown: "`inline code` and\n```\ncode block\n```",
|
||||
want: []string{"<code>inline code</code>", "<pre><code>code block"},
|
||||
name: "code",
|
||||
content: `{"type":"doc","content":[` +
|
||||
`{"type":"paragraph","content":[{"type":"text","marks":[{"type":"code"}],"text":"inline code"}]},` +
|
||||
`{"type":"paragraph","content":[{"type":"text","text":" and "}]},` +
|
||||
`{"type":"codeBlock","content":[{"type":"text","text":"code block"}]}` +
|
||||
`]}`,
|
||||
want: []string{"<code>inline code</code>", "<pre><code>code block</code></pre>"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := DocumentData{
|
||||
Title: "Markdown Test",
|
||||
Content: tt.markdown,
|
||||
Title: "ProseMirror Test",
|
||||
Content: tt.content,
|
||||
}
|
||||
|
||||
result, err := RenderHTML(data)
|
||||
@@ -357,14 +387,29 @@ func TestDocumentVersionSignatureStates(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLargeContent(t *testing.T) {
|
||||
// Create a large markdown content
|
||||
var largeContent strings.Builder
|
||||
largeContent.WriteString(`{"type":"doc","content":[`)
|
||||
for i := range 1000 {
|
||||
largeContent.WriteString("# Section ")
|
||||
largeContent.WriteString(string(rune('A' + i%26)))
|
||||
largeContent.WriteString("\n\nThis is a paragraph with **bold** and *italic* text.\n\n")
|
||||
largeContent.WriteString("- List item 1\n- List item 2\n- List item 3\n\n")
|
||||
if i > 0 {
|
||||
largeContent.WriteByte(',')
|
||||
}
|
||||
largeContent.WriteString(`{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Section `)
|
||||
largeContent.WriteByte(byte('A' + i%26))
|
||||
largeContent.WriteString(`"}]},`)
|
||||
largeContent.WriteString(`{"type":"paragraph","content":[` +
|
||||
`{"type":"text","text":"This is a paragraph with "},` +
|
||||
`{"type":"text","marks":[{"type":"bold"}],"text":"bold"},` +
|
||||
`{"type":"text","text":" and "},` +
|
||||
`{"type":"text","marks":[{"type":"italic"}],"text":"italic"},` +
|
||||
`{"type":"text","text":" text."}` +
|
||||
`]},`)
|
||||
largeContent.WriteString(`{"type":"bulletList","content":[` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"List item 1"}]}]},` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"List item 2"}]}]},` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"List item 3"}]}]}` +
|
||||
`]}`)
|
||||
}
|
||||
largeContent.WriteString(`]}`)
|
||||
|
||||
data := DocumentData{
|
||||
Title: "Large Document",
|
||||
@@ -381,8 +426,21 @@ func BenchmarkGenerateHTML(b *testing.B) {
|
||||
now := time.Now()
|
||||
|
||||
data := DocumentData{
|
||||
Title: "Benchmark Document",
|
||||
Content: "# Title\n\nThis is **bold** text with *italic* formatting.\n\n- Item 1\n- Item 2",
|
||||
Title: "Benchmark Document",
|
||||
Content: `{"type":"doc","content":[` +
|
||||
`{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Title"}]},` +
|
||||
`{"type":"paragraph","content":[` +
|
||||
`{"type":"text","text":"This is "},` +
|
||||
`{"type":"text","marks":[{"type":"bold"}],"text":"bold"},` +
|
||||
`{"type":"text","text":" text with "},` +
|
||||
`{"type":"text","marks":[{"type":"italic"}],"text":"italic"},` +
|
||||
`{"type":"text","text":" formatting."}` +
|
||||
`]},` +
|
||||
`{"type":"bulletList","content":[` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 1"}]}]},` +
|
||||
`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item 2"}]}]}` +
|
||||
`]}` +
|
||||
`]}`,
|
||||
Major: 1,
|
||||
Classification: ClassificationPublic,
|
||||
Approvers: []string{"John Doe"},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -9,6 +10,7 @@
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2.5cm;
|
||||
|
||||
@bottom-right {
|
||||
content: "Page " counter(page) " of " counter(pages);
|
||||
font-family: Arial, sans-serif;
|
||||
@@ -32,14 +34,15 @@
|
||||
width: 21cm;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Individual page sections */
|
||||
.page-section {
|
||||
padding: 2.5cm;
|
||||
position: relative;
|
||||
min-height: 24.7cm; /* A4 height minus padding */
|
||||
min-height: 24.7cm;
|
||||
/* A4 height minus padding */
|
||||
border-bottom: 2px dashed #ddd;
|
||||
page-break-after: always;
|
||||
}
|
||||
@@ -75,7 +78,7 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.company-header + .document-title {
|
||||
.company-header+.document-title {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@@ -248,16 +251,23 @@
|
||||
}
|
||||
|
||||
/* Prevent bad page breaks */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
p, li {
|
||||
p,
|
||||
li {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
table, .signatures-section {
|
||||
table,
|
||||
.signatures-section {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
@@ -367,6 +377,7 @@
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="document-container">
|
||||
<div class="page-section">
|
||||
@@ -394,9 +405,9 @@
|
||||
{{index .Approvers 0}}
|
||||
{{- else}}
|
||||
<ul style="margin: 0; padding-left: 18px;">
|
||||
{{- range .Approvers}}
|
||||
{{- range .Approvers}}
|
||||
<li>{{.}}</li>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
</ul>
|
||||
{{- end}}
|
||||
</td>
|
||||
@@ -417,7 +428,7 @@
|
||||
</div>
|
||||
|
||||
<div class="document-content">
|
||||
{{.Content | formatContent}}
|
||||
{{.BodyHTML}}
|
||||
</div>
|
||||
|
||||
{{- if .Signatures}}
|
||||
@@ -460,4 +471,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -110,6 +110,16 @@ const (
|
||||
MarkLink MarkType = "link"
|
||||
)
|
||||
|
||||
// Parse unmarshals a ProseMirror/Tiptap JSON document string into a Node tree.
|
||||
// Nested content and marks are unmarshaled into Go structs recursively.
|
||||
func Parse(s string) (Node, error) {
|
||||
var n Node
|
||||
if err := json.Unmarshal([]byte(s), &n); err != nil {
|
||||
return Node{}, fmt.Errorf("cannot parse prosemirror node: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// HeadingAttrs parses and returns the heading attributes from a heading node.
|
||||
func (n Node) HeadingAttrs() (HeadingAttrs, error) {
|
||||
var a HeadingAttrs
|
||||
@@ -121,6 +131,9 @@ func (n Node) HeadingAttrs() (HeadingAttrs, error) {
|
||||
|
||||
// CodeBlockAttrs parses and returns the code block attributes.
|
||||
func (n Node) CodeBlockAttrs() (CodeBlockAttrs, error) {
|
||||
if len(n.Attrs) == 0 {
|
||||
return CodeBlockAttrs{}, nil
|
||||
}
|
||||
var a CodeBlockAttrs
|
||||
if err := json.Unmarshal(n.Attrs, &a); err != nil {
|
||||
return a, fmt.Errorf("cannot parse code block attrs: %w", err)
|
||||
|
||||
96
pkg/prosemirror/testdata/document.html
vendored
96
pkg/prosemirror/testdata/document.html
vendored
@@ -1,95 +1 @@
|
||||
<h1>Heading 1</h1>
|
||||
<p>This is a paragraph <strong>with some bold</strong> and <em>some italic </em>and some <u>underlined text</u>.<br>It
|
||||
contains a line break, and some <s>strikethrough.</s><br>There's some <code>inline code</code>. And a <a
|
||||
href="https://getprobo.com" target="_blank" rel="noopener noreferrer nofollow">link</a></p>
|
||||
<h2>Heading 2</h2>
|
||||
<p>A simple paragraph.</p>
|
||||
<pre><code>code block</code></pre>
|
||||
<h3>Heading 3</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>ul <strong>list</strong> item 1</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>ul <em>list</em> item 2</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>ul <a href="https://google.com" target="_blank" rel="noopener noreferrer nofollow">list</a> item 3</p>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<ol>
|
||||
<li>
|
||||
<p>ol list <u>item</u> 1</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>ol list <code>item</code> 2</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>ol list <s>item</s> 3</p>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<blockquote>
|
||||
<p>Blockquote<br>Line <strong>break</strong></p>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
<p>th 1</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>th 2</p>
|
||||
</th>
|
||||
<th>
|
||||
<p>th 3</p>
|
||||
</th>
|
||||
<th style="min-width: 61px">
|
||||
<p>th 4</p>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>td <em>11</em></p>
|
||||
</td>
|
||||
<td>
|
||||
<p>td <strong>12</strong></p>
|
||||
</td>
|
||||
<td>
|
||||
<p>td <s>13</s></p>
|
||||
</td>
|
||||
<td style="min-width: 61px">
|
||||
<ul>
|
||||
<li>
|
||||
<p>1</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>2</p>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>td <u>21</u></p>
|
||||
</td>
|
||||
<td>
|
||||
<p>td <a href="https://example.org" target="_blank" rel="noopener noreferrer nofollow">22</a></p>
|
||||
</td>
|
||||
<td>
|
||||
<p>td <code>23</code></p>
|
||||
</td>
|
||||
<td style="min-width: 61px">
|
||||
<ol>
|
||||
<li>
|
||||
<p>A</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>B</p>
|
||||
</li>
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p></p>
|
||||
<h1>Heading 1</h1><p>This is a paragraph <strong>with some bold</strong> and <em>some italic </em>and some <u>underlined text</u>.<br>It contains a line break, and some <s>strikethrough.</s><br>There's some <code>inline code</code>. And a <a href="https://getprobo.com" target="_blank" rel="noopener noreferrer nofollow">link</a></p><h2>Heading 2</h2><p>A simple paragraph.</p><pre><code>code block</code></pre><h3>Heading 3</h3><ul><li><p>ul <strong>list</strong> item 1</p></li><li><p>ul <em>list</em> item 2</p></li><li><p>ul <a href="https://google.com" target="_blank" rel="noopener noreferrer nofollow">list</a> item 3</p></li></ul><hr><ol><li><p>ol list <u>item</u> 1</p></li><li><p>ol list <code>item</code> 2</p></li><li><p>ol list <s>item</s> 3</p></li></ol><hr><blockquote><p>Blockquote<br>Line <strong>break</strong></p></blockquote><hr><table><tr><th><p>th 1</p></th><th><p>th 2</p></th><th><p>th 3</p></th><th style="min-width: 61px"><p>th 4</p></th></tr><tr><td><p>td <em>11</em></p></td><td><p>td <strong>12</strong></p></td><td><p>td <s>13</s></p></td><td style="min-width: 61px"><ul><li><p>1</p></li><li><p>2</p></li></ul></td></tr><tr><td><p>td <u>21</u></p></td><td><p>td <a href="https://example.org" target="_blank" rel="noopener noreferrer nofollow">22</a></p></td><td><p>td <code>23</code></p></td><td style="min-width: 61px"><ol><li><p>A</p></li><li><p>B</p></li></ol></td></tr></table><p></p>
|
||||
Reference in New Issue
Block a user