+ }
+ }
+}
+
+// getTranslation gibt die Übersetzung zurück oder den Schlüssel, wenn nicht vorhanden
+func getTranslation(translations map[string]string, key string) string {
+ if val, ok := translations[key]; ok {
+ return val
+ }
+ return key
+}
+
templ EditBar(translations map[string]string) {
diff --git a/assets/pad/pad_templ.go b/assets/pad/pad_templ.go
index ccf2406..623e38b 100644
--- a/assets/pad/pad_templ.go
+++ b/assets/pad/pad_templ.go
@@ -11,12 +11,12 @@ import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/ether/etherpad-go/lib/hooks"
padModel "github.com/ether/etherpad-go/lib/models"
- "github.com/ether/etherpad-go/lib/plugins"
+ pluginTypes "github.com/ether/etherpad-go/lib/models/plugins"
"github.com/ether/etherpad-go/lib/settings"
)
// PluginToolbarButtons rendert die Toolbar-Buttons für alle aktivierten Plugins
-func PluginToolbarButtons(translations map[string]string) templ.Component {
+func PluginToolbarButtons(translations map[string]string, buttonGroups []pluginTypes.ToolbarButtonGroup) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -37,7 +37,7 @@ func PluginToolbarButtons(translations map[string]string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- for _, group := range plugins.GetToolbarButtonGroups() {
+ for _, group := range buttonGroups {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -209,7 +209,7 @@ func getTranslation(translations map[string]string, key string) string {
return key
}
-func EditBar(translations map[string]string) templ.Component {
+func EditBar(translations map[string]string, buttonGroups []pluginTypes.ToolbarButtonGroup) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -806,7 +806,7 @@ func EditBar(translations map[string]string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = PluginToolbarButtons(translations).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = PluginToolbarButtons(translations, buttonGroups).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -2409,7 +2409,7 @@ func ChatBox(translations map[string]string) templ.Component {
})
}
-func PadIndex(pad padModel.Model, jsScript string, translations map[string]string, settings *settings.Settings, availableFonts []string) templ.Component {
+func PadIndex(pad padModel.Model, jsScript string, translations map[string]string, settings *settings.Settings, availableFonts []string, buttonGroups []pluginTypes.ToolbarButtonGroup) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -2473,7 +2473,7 @@ func PadIndex(pad padModel.Model, jsScript string, translations map[string]strin
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = EditBar(translations).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = EditBar(translations, buttonGroups).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/lib/api/io/exportHandler.go b/lib/api/io/exportHandler.go
index da48e50..fc166b9 100644
--- a/lib/api/io/exportHandler.go
+++ b/lib/api/io/exportHandler.go
@@ -14,13 +14,18 @@ func GetExport(ctx *fiber.Ctx, exportHandler *io.ExportEtherpad, settings *setti
padId := ctx.Params("pad")
rev := ctx.Params("rev")
exportType := ctx.Params("type")
- typesToExport := []string{
- "pdf", "doc", "docx", "txt", "html", "odt", "etherpad",
+ typesToExport := map[string]string{
+ "pdf": "pdf",
+ "word": "docx",
+ "txt": "txt",
+ "html": "html",
+ "open": "odt",
+ "etherpad": "etherpad",
}
// All formats are now supported internally, no external tools needed
- externalTypes := []string{}
+ var externalTypes []string
- if !slices.Contains(typesToExport, exportType) {
+ if _, ok := typesToExport[exportType]; !ok {
return ctx.Status(400).SendString("Invalid export type")
}
@@ -51,7 +56,7 @@ func GetExport(ctx *fiber.Ctx, exportHandler *io.ExportEtherpad, settings *setti
logger.Infof("Exporting pad %s revision %s to %s", padId, rev, exportType)
- return exportHandler.DoExport(ctx, padId, readOnlyId, exportType)
+ return exportHandler.DoExport(ctx, padId, readOnlyId, typesToExport[exportType])
}
return ctx.Status(401).SendString("Unauthorized to access this pad")
diff --git a/lib/hooks/events/exportEvents.go b/lib/hooks/events/exportEvents.go
new file mode 100644
index 0000000..9ddf6d3
--- /dev/null
+++ b/lib/hooks/events/exportEvents.go
@@ -0,0 +1,49 @@
+package events
+
+import (
+ "github.com/ether/etherpad-go/lib/apool"
+ "github.com/ether/etherpad-go/lib/models"
+)
+
+// LinePDFForExportContext is the context for the getLinePDFForExport hook
+type LinePDFForExportContext struct {
+ Line *models.LineModel
+ LineContent *string
+ Apool *apool.APool
+ AttribLine *string
+ Text *string
+ PadId *string
+ Alignment *string // "left", "center", "right", "justify"
+}
+
+// LineDocxForExportContext is the context for the getLineDocxForExport hook
+type LineDocxForExportContext struct {
+ Line *models.LineModel
+ LineContent *string
+ Apool *apool.APool
+ AttribLine *string
+ Text *string
+ PadId *string
+ Alignment *string // "left", "center", "right", "justify"
+}
+
+// LineOdtForExportContext is the context for the getLineOdtForExport hook
+type LineOdtForExportContext struct {
+ Line *models.LineModel
+ LineContent *string
+ Apool *apool.APool
+ AttribLine *string
+ Text *string
+ PadId *string
+ Alignment *string // "left", "center", "right", "justify"
+}
+
+// LineTxtForExportContext is the context for the getLineTxtForExport hook
+type LineTxtForExportContext struct {
+ Line *models.LineModel
+ LineContent *string
+ Apool *apool.APool
+ AttribLine *string
+ Text *string
+ PadId *string
+}
diff --git a/lib/hooks/events/getLineHTMLForExport.go b/lib/hooks/events/getLineHTMLForExport.go
new file mode 100644
index 0000000..ffd1ce3
--- /dev/null
+++ b/lib/hooks/events/getLineHTMLForExport.go
@@ -0,0 +1,15 @@
+package events
+
+import (
+ "github.com/ether/etherpad-go/lib/apool"
+ "github.com/ether/etherpad-go/lib/models"
+)
+
+type LineHtmlForExportContext struct {
+ Line *models.LineModel
+ LineContent *string
+ Apool *apool.APool
+ AttribLine *string
+ Text *string
+ PadId *string
+}
diff --git a/lib/hooks/hook.go b/lib/hooks/hook.go
index 58b0ed3..27eba2e 100644
--- a/lib/hooks/hook.go
+++ b/lib/hooks/hook.go
@@ -14,6 +14,14 @@ func NewHook() Hook {
}
}
+func (h *Hook) EnqueueGetLineHtmlForExportHook(ctx func(hookName string, ctx any)) {
+ h.EnqueueHook("getLineHTMLForExport", ctx)
+}
+
+func (h *Hook) ExecuteGetLineHtmlForExportHooks(ctx any) {
+ h.ExecuteHooks("getLineHTMLForExport", ctx)
+}
+
func (h *Hook) EnqueueHook(key string, ctx func(hookName string, ctx any)) string {
var uuid = utils.UUID()
var _, ok = h.hooks[key]
diff --git a/lib/io/exportDocx.go b/lib/io/exportDocx.go
index 4b467b4..de228cb 100644
--- a/lib/io/exportDocx.go
+++ b/lib/io/exportDocx.go
@@ -11,18 +11,22 @@ import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/author"
"github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/hooks"
+ "github.com/ether/etherpad-go/lib/hooks/events"
padLib "github.com/ether/etherpad-go/lib/pad"
)
type ExportDocx struct {
padManager *padLib.Manager
authorManager *author.Manager
+ Hooks *hooks.Hook
}
-func NewExportDocx(padManager *padLib.Manager, authorManager *author.Manager) *ExportDocx {
+func NewExportDocx(padManager *padLib.Manager, authorManager *author.Manager, hooksSystem *hooks.Hook) *ExportDocx {
return &ExportDocx{
padManager: padManager,
authorManager: authorManager,
+ Hooks: hooksSystem,
}
}
@@ -39,6 +43,7 @@ type docxParagraph struct {
segments []docxTextSegment
listType string // "bullet", "number", or ""
listLevel int // 0-based level
+ alignment string // "left", "center", "right", "justify"
}
func (e *ExportDocx) GetPadDocxDocument(padId string, optRevNum *int) ([]byte, error) {
@@ -82,6 +87,21 @@ func (e *ExportDocx) GetPadDocxDocument(padId string, optRevNum *int) ([]byte, e
return nil, err
}
+ // Call hook to allow plugins to modify the paragraph (e.g., set alignment)
+ hookContext := &events.LineDocxForExportContext{
+ Apool: &retrievedPad.Pool,
+ AttribLine: &aline,
+ Text: &lineText,
+ PadId: &padId,
+ Alignment: nil,
+ }
+ e.Hooks.ExecuteHooks("getLineDocxForExport", hookContext)
+
+ // Apply alignment from hook if set
+ if hookContext.Alignment != nil {
+ para.alignment = *hookContext.Alignment
+ }
+
paragraphs = append(paragraphs, para)
}
@@ -127,15 +147,33 @@ func (e *ExportDocx) generateDocumentXML(paragraphs []docxParagraph) string {
for _, para := range paragraphs {
bodyContent.WriteString("")
- // Add paragraph properties for lists
- if para.listType != "" {
+ // Add paragraph properties for lists and alignment
+ if para.listType != "" || para.alignment != "" {
bodyContent.WriteString("")
- // numId 1 = bullet, numId 2 = numbered
- numId := 1
- if para.listType == "number" {
- numId = 2
+
+ // Add alignment if set
+ if para.alignment != "" {
+ alignVal := "left"
+ switch para.alignment {
+ case "center":
+ alignVal = "center"
+ case "right":
+ alignVal = "right"
+ case "justify":
+ alignVal = "both"
+ }
+ bodyContent.WriteString(fmt.Sprintf(``, alignVal))
+ }
+
+ // Add list properties
+ if para.listType != "" {
+ // numId 1 = bullet, numId 2 = numbered
+ numId := 1
+ if para.listType == "number" {
+ numId = 2
+ }
+ bodyContent.WriteString(fmt.Sprintf(``, para.listLevel-1, numId))
}
- bodyContent.WriteString(fmt.Sprintf(``, para.listLevel-1, numId))
bodyContent.WriteString("")
}
@@ -208,7 +246,7 @@ func (e *ExportDocx) parseLineSegments(text string, aline string, padPool *apool
return para, nil
}
- // Check for list markers
+ // Check for list markers and alignment
if aline != "" {
ops, err := changeset.DeserializeOps(aline)
if err != nil {
@@ -217,19 +255,36 @@ func (e *ExportDocx) parseLineSegments(text string, aline string, padPool *apool
if len(*ops) > 0 {
op := (*ops)[0]
attribs := changeset.FromString(op.Attribs, padPool)
+
+ // Check for align attribute and remove leading * marker
+ alignStr := attribs.Get("align")
+ if alignStr != nil {
+ para.alignment = *alignStr
+ // Remove the leading * marker for aligned lines
+ if len(text) > 0 && text[0] == '*' {
+ text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return para, err
+ }
+ aline = *newAline
+ }
+ }
+
listTypeStr := attribs.Get("list")
if listTypeStr != nil {
// Parse list type and level (e.g., "bullet1", "number2")
para.listType, para.listLevel = parseListType(*listTypeStr)
- if len(text) > 0 {
+ // Remove leading * if not already removed by align
+ if len(text) > 0 && text[0] == '*' {
text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return para, err
+ }
+ aline = *newAline
}
- newAline, err := changeset.Subattribution(aline, 1, nil)
- if err != nil {
- return para, err
- }
- aline = *newAline
}
}
}
diff --git a/lib/io/exportEtherpad.go b/lib/io/exportEtherpad.go
index ac9abab..03f01c9 100644
--- a/lib/io/exportEtherpad.go
+++ b/lib/io/exportEtherpad.go
@@ -38,9 +38,9 @@ func NewExportEtherpad(hooks *hooks.Hook, padManager *pad.Manager, db db.DataSto
PadManager: padManager,
AuthorManager: authorMgr,
exportTxt: &exportTxt,
- exportDocx: NewExportDocx(padManager, authorMgr),
- exportOdt: NewExportOdt(padManager, authorMgr),
- exportHtml: NewExportHtml(padManager, authorMgr),
+ exportDocx: NewExportDocx(padManager, authorMgr, hooks),
+ exportOdt: NewExportOdt(padManager, authorMgr, hooks),
+ exportHtml: NewExportHtml(padManager, authorMgr, hooks),
logger: logger,
}
@@ -51,6 +51,7 @@ func NewExportEtherpad(hooks *hooks.Hook, padManager *pad.Manager, db db.DataSto
exportEtherpad: exportEtherpad,
padManager: padManager,
authorManager: authorMgr,
+ Hooks: hooks,
}
return exportEtherpad
@@ -209,7 +210,7 @@ func (e *ExportEtherpad) DoExport(ctx *fiber.Ctx, id string, readOnlyId *string,
return ctx.Status(500).SendString(err.Error())
}
return ctx.Send(pdfBytes)
- case "doc", "docx":
+ case "doc", "docx", "word":
ctx.Set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
docxBytes, err := e.exportDocx.GetPadDocxDocument(id, optRevNum)
if err != nil {
@@ -217,7 +218,7 @@ func (e *ExportEtherpad) DoExport(ctx *fiber.Ctx, id string, readOnlyId *string,
return ctx.Status(500).SendString(err.Error())
}
return ctx.Send(docxBytes)
- case "odt":
+ case "odt", "open":
ctx.Set("Content-Type", "application/vnd.oasis.opendocument.text")
odtBytes, err := e.exportOdt.GetPadOdtDocument(id, optRevNum)
if err != nil {
diff --git a/lib/io/exportHtml.go b/lib/io/exportHtml.go
index 2c9081b..03e0e6e 100644
--- a/lib/io/exportHtml.go
+++ b/lib/io/exportHtml.go
@@ -11,6 +11,8 @@ import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/author"
"github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/hooks"
+ "github.com/ether/etherpad-go/lib/hooks/events"
padModel "github.com/ether/etherpad-go/lib/models/pad"
padLib "github.com/ether/etherpad-go/lib/pad"
)
@@ -18,12 +20,14 @@ import (
type ExportHtml struct {
PadManager *padLib.Manager
AuthorManager *author.Manager
+ Hooks *hooks.Hook
}
-func NewExportHtml(padManager *padLib.Manager, authorManager *author.Manager) *ExportHtml {
+func NewExportHtml(padManager *padLib.Manager, authorManager *author.Manager, hooks *hooks.Hook) *ExportHtml {
return &ExportHtml{
PadManager: padManager,
AuthorManager: authorManager,
+ Hooks: hooks,
}
}
@@ -78,11 +82,11 @@ func (e *ExportHtml) GetPadHTML(pad *padModel.Pad, revNum *int, authorColors map
}
}
- return e.getHTMLFromAtext(&pad.Pool, atext, authorColors)
+ return e.getHTMLFromAtext(pad.Id, &pad.Pool, atext, authorColors)
}
// getHTMLFromAtext converts an AText to HTML
-func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, authorColors map[string]string) (string, error) {
+func (e *ExportHtml) getHTMLFromAtext(padId string, padPool *apool.APool, atext apool.AText, authorColors map[string]string) (string, error) {
textLines := padLib.SplitRemoveLastRune(atext.Text)
attribLines, err := changeset.SplitAttributionLines(atext.Attribs, atext.Text)
if err != nil {
@@ -362,7 +366,17 @@ func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, a
return "", err
}
+ // If we are inside a list
if line.ListLevel > 0 {
+ hookContext := &events.LineHtmlForExportContext{
+ Line: line,
+ LineContent: &lineContent,
+ Apool: padPool,
+ AttribLine: &attribLines[i],
+ Text: &textLines[i],
+ PadId: &padId,
+ }
+
var prevLine *padLib.LineModel
var nextLine *padLib.LineModel
@@ -377,7 +391,9 @@ func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, a
nextLine, _ = padLib.AnalyzeLine(textLines[i+1], nextAline, *padPool)
}
- // Create list parent elements
+ e.Hooks.ExecuteHooks("getLineHTMLForExport", hookContext)
+
+ // To create list parent elements
if prevLine == nil || prevLine.ListLevel != line.ListLevel || line.ListTypeName != prevLine.ListTypeName {
exists := listExists(openLists, line.ListLevel, line.ListTypeName)
if !exists {
@@ -403,7 +419,13 @@ func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, a
}
if line.ListTypeName == "number" {
- pieces = append(pieces, "")
+ // We introduce line.start here, this is useful for continuing
+ // Ordered list line numbers
+ if line.Start != "" {
+ pieces = append(pieces, "")
+ } else {
+ pieces = append(pieces, "")
+ }
} else {
pieces = append(pieces, "
")
}
@@ -411,18 +433,29 @@ func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, a
}
}
- // Add list item content
- if lineContent != "" {
- pieces = append(pieces, "
", lineContent)
+ // if we're going up a level we shouldn't be adding..
+ if hookContext.LineContent != nil && *hookContext.LineContent != "" {
+ pieces = append(pieces, "
", *hookContext.LineContent)
}
- // Check if we need to close lists
- needsListClose := nextLine == nil ||
+ // To close list elements
+ if nextLine != nil &&
+ nextLine.ListLevel == line.ListLevel &&
+ line.ListTypeName == nextLine.ListTypeName {
+ if hookContext.LineContent != nil && *hookContext.LineContent != "" {
+ if nextLine.ListTypeName == "number" && string(nextLine.Text) == "" {
+ // don't do anything because the next item is a nested ol opener so we need to
+ // keep the li open
+ } else {
+ pieces = append(pieces, "
")
+ }
+ }
+ }
+
+ if nextLine == nil ||
nextLine.ListLevel == 0 ||
nextLine.ListLevel < line.ListLevel ||
- line.ListTypeName != nextLine.ListTypeName
-
- if needsListClose {
+ line.ListTypeName != nextLine.ListTypeName {
nextLevel := 0
if nextLine != nil && nextLine.ListLevel > 0 {
nextLevel = nextLine.ListLevel
@@ -431,33 +464,36 @@ func (e *ExportHtml) getHTMLFromAtext(padPool *apool.APool, atext apool.AText, a
nextLevel = 0
}
- // Close the current li before closing the list
- if lineContent != "" {
- pieces = append(pieces, "")
- }
-
for diff := nextLevel; diff < line.ListLevel; diff++ {
openLists = filterList(openLists, diff, line.ListTypeName)
+ if len(pieces) > 0 {
+ lastPiece := pieces[len(pieces)-1]
+ if strings.HasPrefix(lastPiece, "
")
+ }
+ }
+
if line.ListTypeName == "number" {
pieces = append(pieces, "")
} else {
pieces = append(pieces, "")
}
}
- } else if lineContent != "" {
- // Close list item if next line continues same list
- if nextLine != nil &&
- nextLine.ListLevel == line.ListLevel &&
- line.ListTypeName == nextLine.ListTypeName {
- if !(nextLine.ListTypeName == "number" && string(nextLine.Text) == "") {
- pieces = append(pieces, "")
- }
- }
}
} else {
- // Outside any list
- pieces = append(pieces, lineContent, " ")
+ // outside any list
+ hookContext := &events.LineHtmlForExportContext{
+ Line: line,
+ LineContent: &lineContent,
+ Apool: padPool,
+ AttribLine: &attribLines[i],
+ Text: &textLines[i],
+ PadId: &padId,
+ }
+
+ e.Hooks.ExecuteHooks("getLineHTMLForExport", hookContext)
+ pieces = append(pieces, *hookContext.LineContent, " ")
}
}
diff --git a/lib/io/exportOdt.go b/lib/io/exportOdt.go
index 7f7421e..0ba893e 100644
--- a/lib/io/exportOdt.go
+++ b/lib/io/exportOdt.go
@@ -11,18 +11,22 @@ import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/author"
"github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/hooks"
+ "github.com/ether/etherpad-go/lib/hooks/events"
padLib "github.com/ether/etherpad-go/lib/pad"
)
type ExportOdt struct {
padManager *padLib.Manager
authorManager *author.Manager
+ Hooks *hooks.Hook
}
-func NewExportOdt(padManager *padLib.Manager, authorManager *author.Manager) *ExportOdt {
+func NewExportOdt(padManager *padLib.Manager, authorManager *author.Manager, hooksSystem *hooks.Hook) *ExportOdt {
return &ExportOdt{
padManager: padManager,
authorManager: authorManager,
+ Hooks: hooksSystem,
}
}
@@ -39,6 +43,7 @@ type odtParagraph struct {
segments []odtTextSegment
listType string // "bullet", "number", or ""
listLevel int // 1-based level
+ alignment string // "left", "center", "right", "justify"
}
func (e *ExportOdt) GetPadOdtDocument(padId string, optRevNum *int) ([]byte, error) {
@@ -82,6 +87,21 @@ func (e *ExportOdt) GetPadOdtDocument(padId string, optRevNum *int) ([]byte, err
return nil, err
}
+ // Call hook to allow plugins to modify the paragraph (e.g., set alignment)
+ hookContext := &events.LineOdtForExportContext{
+ Apool: &retrievedPad.Pool,
+ AttribLine: &aline,
+ Text: &lineText,
+ PadId: &padId,
+ Alignment: nil,
+ }
+ e.Hooks.ExecuteHooks("getLineOdtForExport", hookContext)
+
+ // Apply alignment from hook if set
+ if hookContext.Alignment != nil {
+ para.alignment = *hookContext.Alignment
+ }
+
paragraphs = append(paragraphs, para)
}
@@ -167,6 +187,12 @@ func (e *ExportOdt) generateContentXML(paragraphs []odtParagraph, authorColors m
// Generate combined styles for multiple formatting
automaticStyles.WriteString(``)
+ // Alignment paragraph styles
+ automaticStyles.WriteString(``)
+ automaticStyles.WriteString(``)
+ automaticStyles.WriteString(``)
+ automaticStyles.WriteString(``)
+
// Bullet list style
automaticStyles.WriteString(``)
automaticStyles.WriteString(``)
@@ -190,6 +216,19 @@ func (e *ExportOdt) generateContentXML(paragraphs []odtParagraph, authorColors m
inNumberList := false
for _, para := range paragraphs {
+ // Determine paragraph style based on alignment
+ paraStyle := "Standard"
+ switch para.alignment {
+ case "left":
+ paraStyle = "PLeft"
+ case "center":
+ paraStyle = "PCenter"
+ case "right":
+ paraStyle = "PRight"
+ case "justify":
+ paraStyle = "PJustify"
+ }
+
// Handle list transitions
if para.listType == "bullet" {
if inNumberList {
@@ -201,7 +240,7 @@ func (e *ExportOdt) generateContentXML(paragraphs []odtParagraph, authorColors m
inBulletList = true
}
bodyContent.WriteString("")
- bodyContent.WriteString("")
+ bodyContent.WriteString(fmt.Sprintf(``, paraStyle))
} else if para.listType == "number" {
if inBulletList {
bodyContent.WriteString("")
@@ -212,7 +251,7 @@ func (e *ExportOdt) generateContentXML(paragraphs []odtParagraph, authorColors m
inNumberList = true
}
bodyContent.WriteString("")
- bodyContent.WriteString("")
+ bodyContent.WriteString(fmt.Sprintf(``, paraStyle))
} else {
if inBulletList {
bodyContent.WriteString("")
@@ -222,7 +261,7 @@ func (e *ExportOdt) generateContentXML(paragraphs []odtParagraph, authorColors m
bodyContent.WriteString("")
inNumberList = false
}
- bodyContent.WriteString("")
+ bodyContent.WriteString(fmt.Sprintf(``, paraStyle))
}
// Write segments
@@ -311,7 +350,7 @@ func (e *ExportOdt) parseLineSegments(text string, aline string, padPool *apool.
return para, nil
}
- // Check for list markers
+ // Check for list markers and alignment
if aline != "" {
ops, err := changeset.DeserializeOps(aline)
if err != nil {
@@ -320,18 +359,35 @@ func (e *ExportOdt) parseLineSegments(text string, aline string, padPool *apool.
if len(*ops) > 0 {
op := (*ops)[0]
attribs := changeset.FromString(op.Attribs, padPool)
+
+ // Check for align attribute and remove leading * marker
+ alignStr := attribs.Get("align")
+ if alignStr != nil {
+ para.alignment = *alignStr
+ // Remove the leading * marker for aligned lines
+ if len(text) > 0 && text[0] == '*' {
+ text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return para, err
+ }
+ aline = *newAline
+ }
+ }
+
listTypeStr := attribs.Get("list")
if listTypeStr != nil {
para.listType, para.listLevel = parseListTypeOdt(*listTypeStr)
- if len(text) > 0 {
+ // Remove leading * if not already removed by align
+ if len(text) > 0 && text[0] == '*' {
text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return para, err
+ }
+ aline = *newAline
}
- newAline, err := changeset.Subattribution(aline, 1, nil)
- if err != nil {
- return para, err
- }
- aline = *newAline
}
}
}
diff --git a/lib/io/exportPDF.go b/lib/io/exportPDF.go
index c16fa25..b06b061 100644
--- a/lib/io/exportPDF.go
+++ b/lib/io/exportPDF.go
@@ -12,6 +12,8 @@ import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/author"
"github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/hooks"
+ "github.com/ether/etherpad-go/lib/hooks/events"
"github.com/ether/etherpad-go/lib/models/pad"
padLib "github.com/ether/etherpad-go/lib/pad"
"github.com/pdfcpu/pdfcpu/pkg/api"
@@ -25,6 +27,7 @@ type ExportPDF struct {
uiAssets embed.FS
padManager *padLib.Manager
authorManager *author.Manager
+ Hooks *hooks.Hook
}
const (
@@ -48,8 +51,9 @@ type textSegment struct {
}
type listInfo struct {
- listType string // "bullet", "number", or ""
- level int // 1-based level for nested lists
+ listType string // "bullet", "number", or ""
+ level int // 1-based level for nested lists
+ alignment string // "left", "center", "right", "justify"
}
func (e *ExportPDF) GetPadPdfDocument(padId string, optRevNum *int) ([]byte, error) {
@@ -234,6 +238,22 @@ func (e *ExportPDF) renderFormattedText(pdf *gopdf.GoPdf, retrievedPad *pad.Pad,
return err
}
+ // Call hook to allow plugins to modify the line (e.g., set alignment)
+ padId := retrievedPad.Id
+ hookContext := &events.LinePDFForExportContext{
+ Apool: &padPool,
+ AttribLine: &aline,
+ Text: &lineText,
+ PadId: &padId,
+ Alignment: nil,
+ }
+ e.Hooks.ExecuteHooks("getLinePDFForExport", hookContext)
+
+ // Apply alignment from hook if set
+ if hookContext.Alignment != nil {
+ listInfo.alignment = *hookContext.Alignment
+ }
+
// Handle list prefix with proper numbering
if listInfo.listType != "" {
// Calculate indentation based on list level
@@ -267,7 +287,31 @@ func (e *ExportPDF) renderFormattedText(pdf *gopdf.GoPdf, retrievedPad *pad.Pad,
// Reset list tracking when not in a list
lastListType = ""
lastListLevel = 0
- pdf.SetX(marginLeft)
+
+ // Apply alignment for non-list content
+ if listInfo.alignment != "" && listInfo.alignment != "left" {
+ // Calculate total text width
+ totalWidth := 0.0
+ for _, seg := range segments {
+ if err := pdf.SetFont("Roboto", "", fontSize); err != nil {
+ return err
+ }
+ w, _ := pdf.MeasureTextWidth(seg.text)
+ totalWidth += w
+ }
+
+ availableWidth := pageWidth - marginLeft - marginRight
+ switch listInfo.alignment {
+ case "center":
+ pdf.SetX(marginLeft + (availableWidth-totalWidth)/2)
+ case "right":
+ pdf.SetX(marginLeft + availableWidth - totalWidth)
+ default:
+ pdf.SetX(marginLeft)
+ }
+ } else {
+ pdf.SetX(marginLeft)
+ }
}
for _, seg := range segments {
@@ -323,6 +367,22 @@ func (e *ExportPDF) parseLineSegments(text string, aline string, padPool *apool.
if len(*ops) > 0 {
op := (*ops)[0]
attribs := changeset.FromString(op.Attribs, padPool)
+
+ // Check for align attribute and remove leading * marker
+ alignStr := attribs.Get("align")
+ if alignStr != nil {
+ info.alignment = *alignStr
+ // Remove the leading * marker for aligned lines
+ if len(text) > 0 && text[0] == '*' {
+ text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return nil, info, err
+ }
+ aline = *newAline
+ }
+ }
+
listTypeStr := attribs.Get("list")
if listTypeStr != nil {
// Parse list type and level (e.g., "bullet1", "number2")
@@ -346,15 +406,15 @@ func (e *ExportPDF) parseLineSegments(text string, aline string, padPool *apool.
}
}
- // Remove the leading * marker from list items
+ // Remove the leading * marker from list items (if not already removed by align)
if len(text) > 0 && text[0] == '*' {
text = text[1:]
+ newAline, err := changeset.Subattribution(aline, 1, nil)
+ if err != nil {
+ return nil, info, err
+ }
+ aline = *newAline
}
- newAline, err := changeset.Subattribution(aline, 1, nil)
- if err != nil {
- return nil, info, err
- }
- aline = *newAline
}
}
}
diff --git a/lib/models/LineModel.go b/lib/models/LineModel.go
new file mode 100644
index 0000000..fdce1a9
--- /dev/null
+++ b/lib/models/LineModel.go
@@ -0,0 +1,10 @@
+package models
+
+// LineModel represents a parsed line with its attributes for export
+type LineModel struct {
+ ListLevel int
+ Text []rune
+ Aline string
+ ListTypeName string
+ Start string
+}
diff --git a/lib/models/plugins/toolbar.go b/lib/models/plugins/toolbar.go
new file mode 100644
index 0000000..40b2f70
--- /dev/null
+++ b/lib/models/plugins/toolbar.go
@@ -0,0 +1,16 @@
+package plugins
+
+// ToolbarButton repräsentiert einen einzelnen Toolbar-Button
+type ToolbarButton struct {
+ Key string `json:"key"`
+ Title string `json:"title"`
+ Icon string `json:"icon"`
+ Group string `json:"group"` // "left", "middle", "right"
+ DataAlign string `json:"dataAlign"`
+}
+
+// ToolbarButtonGroup repräsentiert eine Gruppe von Toolbar-Buttons
+type ToolbarButtonGroup struct {
+ PluginName string
+ Buttons []ToolbarButton
+}
diff --git a/lib/pad/exportTxt.go b/lib/pad/exportTxt.go
index dbc76be..bd6d3f6 100644
--- a/lib/pad/exportTxt.go
+++ b/lib/pad/exportTxt.go
@@ -8,6 +8,7 @@ import (
"github.com/ether/etherpad-go/lib/apool"
"github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/models"
"github.com/ether/etherpad-go/lib/models/pad"
"github.com/ether/etherpad-go/lib/utils"
)
@@ -24,13 +25,8 @@ func SplitRemoveLastRune(s string) []string {
return strings.Split(trimmed, "\n")
}
-type LineModel struct {
- ListLevel int
- Text []rune
- Aline string
- ListTypeName string
- Start string
-}
+// LineModel is an alias for models.LineModel for backwards compatibility
+type LineModel = models.LineModel
func parseListType(listType string) (tag string, num int, ok bool) {
re := regexp.MustCompile(`^([a-z]+)([0-9]+)`)
@@ -56,6 +52,12 @@ func AnalyzeLine(text string, aline string, attrpool apool.APool) (*LineModel, e
op := (*ops)[0]
attribs := changeset.FromString(op.Attribs, &attrpool)
+ // Check for align attribute - also requires removing the leading *
+ alignStr := attribs.Get("align")
+ if alignStr != nil {
+ lineMarker = 1
+ }
+
listTypeStr := attribs.Get("list")
if listTypeStr != nil {
lineMarker = 1
diff --git a/lib/pad/exportTxt_test.go b/lib/pad/exportTxt_test.go
index 8be1481..08a13dd 100644
--- a/lib/pad/exportTxt_test.go
+++ b/lib/pad/exportTxt_test.go
@@ -111,3 +111,80 @@ func TestGetTxtFromAText_BoldText(t *testing.T) {
t.Fatal("result is nil")
}
}
+
+func TestAnalyzeLine_WithAlignAttribute(t *testing.T) {
+ pool := apool.NewAPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "center"}, nil)
+
+ // Text with a leading asterisk (as Etherpad stores aligned lines)
+ line, err := AnalyzeLine("*test\n", "*0|1+6", pool)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // The leading asterisk should be removed
+ if string(line.Text) != "test\n" {
+ t.Errorf("got text %q, want %q (asterisk should be removed)", string(line.Text), "test\n")
+ }
+}
+
+func TestAnalyzeLine_WithAlignAttributeNoAsterisk(t *testing.T) {
+ pool := apool.NewAPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "right"}, nil)
+
+ // Text without leading asterisk (edge case)
+ line, err := AnalyzeLine("test\n", "*0|1+5", pool)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Should handle gracefully even without asterisk - first char is removed
+ if string(line.Text) != "est\n" {
+ t.Errorf("got text %q, want %q", string(line.Text), "est\n")
+ }
+}
+
+func TestAnalyzeLine_WithAlignAndListAttributes(t *testing.T) {
+ pool := apool.NewAPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "center"}, nil)
+ pool.PutAttrib(apool.Attribute{Key: "list", Value: "bullet1"}, nil)
+
+ // Line with both align and list attributes
+ line, err := AnalyzeLine("*item\n", "*0*1|1+6", pool)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Should have list properties
+ if line.ListLevel != 1 {
+ t.Errorf("got ListLevel %d, want 1", line.ListLevel)
+ }
+ if line.ListTypeName != "bullet" {
+ t.Errorf("got ListTypeName %q, want %q", line.ListTypeName, "bullet")
+ }
+ // Asterisk should be removed
+ if string(line.Text) != "item\n" {
+ t.Errorf("got text %q, want %q", string(line.Text), "item\n")
+ }
+}
+
+func TestAnalyzeLine_AllAlignmentTypes(t *testing.T) {
+ alignments := []string{"left", "center", "right", "justify"}
+
+ for _, alignment := range alignments {
+ t.Run(alignment, func(t *testing.T) {
+ pool := apool.NewAPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: alignment}, nil)
+
+ line, err := AnalyzeLine("*Hello\n", "*0|1+7", pool)
+
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // Asterisk should be removed for all alignment types
+ if string(line.Text) != "Hello\n" {
+ t.Errorf("got text %q, want %q for alignment %s", string(line.Text), "Hello\n", alignment)
+ }
+ })
+ }
+}
diff --git a/lib/pad/padFrontend.go b/lib/pad/padFrontend.go
index 292e4f9..5c7795c 100644
--- a/lib/pad/padFrontend.go
+++ b/lib/pad/padFrontend.go
@@ -6,6 +6,7 @@ import (
"github.com/a-h/templ"
"github.com/ether/etherpad-go/lib/models"
+ "github.com/ether/etherpad-go/lib/plugins"
"github.com/ether/etherpad-go/lib/settings"
"github.com/ether/etherpad-go/lib/utils"
"github.com/gofiber/adaptor/v2"
@@ -35,8 +36,9 @@ func HandlePadOpen(c *fiber.Ctx, uiAssets embed.FS, retrievedSettings *settings.
}
jsFilePath := "/js/pad/assets/pad.js?v=" + strconv.Itoa(utils.RandomVersionString)
+ buttonGroups := plugins.GetToolbarButtonGroups()
- padComp := padAsset.PadIndex(pad, jsFilePath, keyValues, retrievedSettings, AvailableFonts)
+ padComp := padAsset.PadIndex(pad, jsFilePath, keyValues, retrievedSettings, AvailableFonts, buttonGroups)
return adaptor.HTTPHandler(templ.Handler(padComp))(c)
}
diff --git a/lib/plugins/PluginDef.go b/lib/plugins/PluginDef.go
index aa75aa4..8a7e22c 100644
--- a/lib/plugins/PluginDef.go
+++ b/lib/plugins/PluginDef.go
@@ -1,13 +1,14 @@
package plugins
-// ToolbarButton definiert einen Toolbar-Button für ein Plugin
-type ToolbarButton struct {
- Key string `json:"key"` // z.B. "alignLeft"
- Title string `json:"title"` // Lokalisierungsschlüssel oder direkter Text
- Icon string `json:"icon"` // CSS-Klasse für das Icon
- Group string `json:"group"` // "left", "middle", "right" für Gruppierung
- DataAlign string `json:"data_align"` // Optional: data-align Attribut
-}
+import (
+ pluginTypes "github.com/ether/etherpad-go/lib/models/plugins"
+)
+
+// ToolbarButton is an alias to the type in models/plugins
+type ToolbarButton = pluginTypes.ToolbarButton
+
+// ToolbarButtonGroup is an alias to the type in models/plugins
+type ToolbarButtonGroup = pluginTypes.ToolbarButtonGroup
// Part repräsentiert einen Teil eines Plugins
type Part struct {
diff --git a/lib/plugins/ep_align/export.go b/lib/plugins/ep_align/export.go
new file mode 100644
index 0000000..0ea4cb8
--- /dev/null
+++ b/lib/plugins/ep_align/export.go
@@ -0,0 +1,100 @@
+package ep_align
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/ether/etherpad-go/lib/apool"
+ "github.com/ether/etherpad-go/lib/changeset"
+ "github.com/ether/etherpad-go/lib/hooks/events"
+)
+
+// analyzeLine extracts the alignment attribute from a line's attribute string.
+// It returns the alignment value (e.g., "left", "center", "right", "justify") or nil if not set.
+func analyzeLine(alineAttrs *string, pool *apool.APool) *string {
+ if alineAttrs == nil {
+ return nil
+ }
+
+ ops, err := changeset.DeserializeOps(*alineAttrs)
+ if err != nil {
+ return nil
+ }
+
+ // Check the first op for the align attribute (like the original JS code)
+ if ops != nil && len(*ops) > 0 {
+ op := (*ops)[0]
+ // Create an AttributeMap from the op's attribs string
+ attributeMap := changeset.FromString(op.Attribs, pool)
+ return attributeMap.Get("align")
+ }
+
+ return nil
+}
+
+// GetLineHTMLForExport wraps a line with the appropriate alignment tag for export.
+func GetLineHTMLForExport(event *events.LineHtmlForExportContext) {
+ align := analyzeLine(event.AttribLine, event.Apool)
+ if align == nil {
+ return
+ }
+
+ lineContent := *event.LineContent
+ text := ""
+ if event.Text != nil {
+ text = *event.Text
+ }
+
+ // Remove leading '*' if present in text
+ if len(text) > 0 && text[0] == '*' {
+ lineContent = strings.Replace(lineContent, "*", "", 1)
+ }
+
+ // Check if there's a heading tag
+ headingRegex := regexp.MustCompile(`]+)?>`)
+ headingMatch := headingRegex.FindString(lineContent)
+
+ if headingMatch != "" {
+ // There's a heading, add style to it
+ if !strings.Contains(headingMatch, "style=") {
+ // No style attribute, add one
+ lineContent = strings.Replace(lineContent, ">", " style='text-align:"+*align+"'>", 1)
+ } else {
+ // Style attribute exists, append to it
+ lineContent = strings.Replace(lineContent, "style=", "style='text-align:"+*align+" ", 1)
+ }
+ } else {
+ // No heading, wrap in a
tag
+ lineContent = "
" + lineContent + "
"
+ }
+
+ // Write the modified content back to the event
+ *event.LineContent = lineContent
+}
+
+// GetLinePDFForExport sets alignment for PDF export
+func GetLinePDFForExport(event *events.LinePDFForExportContext) {
+ align := analyzeLine(event.AttribLine, event.Apool)
+ if align == nil {
+ return
+ }
+ event.Alignment = align
+}
+
+// GetLineDocxForExport sets alignment for DOCX export
+func GetLineDocxForExport(event *events.LineDocxForExportContext) {
+ align := analyzeLine(event.AttribLine, event.Apool)
+ if align == nil {
+ return
+ }
+ event.Alignment = align
+}
+
+// GetLineOdtForExport sets alignment for ODT export
+func GetLineOdtForExport(event *events.LineOdtForExportContext) {
+ align := analyzeLine(event.AttribLine, event.Apool)
+ if align == nil {
+ return
+ }
+ event.Alignment = align
+}
diff --git a/lib/plugins/ep_align/export_test.go b/lib/plugins/ep_align/export_test.go
new file mode 100644
index 0000000..666cae2
--- /dev/null
+++ b/lib/plugins/ep_align/export_test.go
@@ -0,0 +1,383 @@
+package ep_align
+
+import (
+ "testing"
+
+ "github.com/ether/etherpad-go/lib/apool"
+ "github.com/ether/etherpad-go/lib/hooks/events"
+)
+
+// createTestPool creates a test attribute pool with common attributes
+func createTestPool() *apool.APool {
+ pool := &apool.APool{
+ NumToAttrib: make(map[int]apool.Attribute),
+ AttribToNum: make(map[apool.Attribute]int),
+ NextNum: 0,
+ }
+ return pool
+}
+
+// addAlignAttribute adds an align attribute to the pool and returns the attrib string
+func addAlignAttribute(pool *apool.APool, alignValue string) string {
+ num := pool.PutAttrib(apool.Attribute{Key: "align", Value: alignValue}, nil)
+ // Format: *num*
+ return "*" + string(rune('0'+num)) + "*"
+}
+
+func TestAnalyzeLine_NoAlignment(t *testing.T) {
+ pool := createTestPool()
+ aline := ""
+
+ result := analyzeLine(&aline, pool)
+
+ if result != nil {
+ t.Errorf("Expected nil alignment, got %v", *result)
+ }
+}
+
+func TestAnalyzeLine_NilAttribLine(t *testing.T) {
+ pool := createTestPool()
+
+ result := analyzeLine(nil, pool)
+
+ if result != nil {
+ t.Errorf("Expected nil alignment for nil attrib line, got %v", *result)
+ }
+}
+
+func TestAnalyzeLine_CenterAlignment(t *testing.T) {
+ pool := createTestPool()
+ // Add center alignment to pool
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "center"}, nil)
+
+ // Create attrib string: +1*0*1| means 1 char with attrib 0
+ aline := "*0|1+1"
+
+ result := analyzeLine(&aline, pool)
+
+ if result == nil {
+ t.Fatal("Expected alignment, got nil")
+ }
+ if *result != "center" {
+ t.Errorf("Expected 'center' alignment, got '%s'", *result)
+ }
+}
+
+func TestAnalyzeLine_LeftAlignment(t *testing.T) {
+ pool := createTestPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "left"}, nil)
+ aline := "*0|1+1"
+
+ result := analyzeLine(&aline, pool)
+
+ if result == nil {
+ t.Fatal("Expected alignment, got nil")
+ }
+ if *result != "left" {
+ t.Errorf("Expected 'left' alignment, got '%s'", *result)
+ }
+}
+
+func TestAnalyzeLine_RightAlignment(t *testing.T) {
+ pool := createTestPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "right"}, nil)
+ aline := "*0|1+1"
+
+ result := analyzeLine(&aline, pool)
+
+ if result == nil {
+ t.Fatal("Expected alignment, got nil")
+ }
+ if *result != "right" {
+ t.Errorf("Expected 'right' alignment, got '%s'", *result)
+ }
+}
+
+func TestAnalyzeLine_JustifyAlignment(t *testing.T) {
+ pool := createTestPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "justify"}, nil)
+ aline := "*0|1+1"
+
+ result := analyzeLine(&aline, pool)
+
+ if result == nil {
+ t.Fatal("Expected alignment, got nil")
+ }
+ if *result != "justify" {
+ t.Errorf("Expected 'justify' alignment, got '%s'", *result)
+ }
+}
+
+func TestGetLineHTMLForExport_NoAlignment(t *testing.T) {
+ pool := createTestPool()
+ lineContent := "Hello World"
+ text := "Hello World"
+ aline := ""
+ padId := "test-pad"
+
+ event := &events.LineHtmlForExportContext{
+ LineContent: &lineContent,
+ Apool: pool,
+ AttribLine: &aline,
+ Text: &text,
+ PadId: &padId,
+ }
+
+ GetLineHTMLForExport(event)
+
+ // Should remain unchanged when no alignment
+ if *event.LineContent != "Hello World" {
+ t.Errorf("Expected unchanged content, got '%s'", *event.LineContent)
+ }
+}
+
+func TestGetLineHTMLForExport_CenterAlignment(t *testing.T) {
+ pool := createTestPool()
+ pool.PutAttrib(apool.Attribute{Key: "align", Value: "center"}, nil)
+
+ lineContent := "Hello World"
+ text := "*Hello World"
+ aline := "*0|1+1"
+ padId := "test-pad"
+
+ event := &events.LineHtmlForExportContext{
+ LineContent: &lineContent,
+ Apool: pool,
+ AttribLine: &aline,
+ Text: &text,
+ PadId: &padId,
+ }
+
+ GetLineHTMLForExport(event)
+
+ expected := "