diff --git a/go.mod b/go.mod index 20007b870..733e7d778 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0be1f264a..12cd050f3 100644 --- a/go.sum +++ b/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= diff --git a/pkg/docgen/generator.go b/pkg/docgen/generator.go index c2e5516c1..19ccddb7b 100644 --- a/pkg/docgen/generator.go +++ b/pkg/docgen/generator.go @@ -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("
%s
", html.EscapeString(content))) - } - return template.HTML(buf.String()) - }, "imgTag": func(src, alt, class string) template.HTML { return template.HTML(fmt.Sprintf(`%s
", html.EscapeString(s))) + } + htmlStr, err := prosemirror.RenderHTML(node) + if err != nil { + return template.HTML(fmt.Sprintf("%s
", 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) } diff --git a/pkg/docgen/generator_test.go b/pkg/docgen/generator_test.go index c029829b9..95095f935 100644 --- a/pkg/docgen/generator_test.go +++ b/pkg/docgen/generator_test.go @@ -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{ "Item 1
Item 2
code block",
+ "code block",
},
},
{
@@ -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), "bold")
- // 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), "simple text
") - // 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), "**not** json
") + + 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{"Item 1
Item 2
Paragraph 1
", "Paragraph 2
"}, + name: "paragraphs", + content: `{"type":"doc","content":[` + + `{"type":"paragraph","content":[{"type":"text","text":"Paragraph 1"}]},` + + `{"type":"paragraph","content":[{"type":"text","text":"Paragraph 2"}]}` + + `]}`, + want: []string{"Paragraph 1
", "Paragraph 2
"}, }, { - name: "code", - markdown: "`inline code` and\n```\ncode block\n```", - want: []string{"inline 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{"inline code", "code block
"},
},
}
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"},
diff --git a/pkg/docgen/template.html b/pkg/docgen/template.html
index 3aef8cfac..4d70965eb 100644
--- a/pkg/docgen/template.html
+++ b/pkg/docgen/template.html
@@ -1,5 +1,6 @@
+
@@ -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 @@
});
+
@@ -394,9 +405,9 @@
{{index .Approvers 0}}
{{- else}}
- {{- range .Approvers}}
+ {{- range .Approvers}}
- {{.}}
- {{- end}}
+ {{- end}}
{{- end}}
@@ -417,7 +428,7 @@
- {{.Content | formatContent}}
+ {{.BodyHTML}}
{{- if .Signatures}}
@@ -460,4 +471,5 @@
-
+
+