Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions assets/pad/pad.templ
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,33 @@ package pad
import (
padModel "github.com/ether/etherpad-go/lib/models"
"github.com/ether/etherpad-go/lib/hooks"
"github.com/ether/etherpad-go/lib/settings"
"github.com/ether/etherpad-go/lib/settings"
pluginTypes "github.com/ether/etherpad-go/lib/models/plugins"
)

templ EditBar(translations map[string]string) {
// PluginToolbarButtons rendert die Toolbar-Buttons für alle aktivierten Plugins
templ PluginToolbarButtons(translations map[string]string, buttonGroups []pluginTypes.ToolbarButtonGroup) {
for _, group := range buttonGroups {
<li class="separator acl-write"></li>
for _, btn := range group.Buttons {
<li data-type="button" data-key={btn.Key} data-l10n-id={btn.Title}>
<a class={"grouped-" + btn.Group + " ep_align"} data-align={btn.DataAlign} data-l10n-id={btn.Title} title={getTranslation(translations, btn.Title)} aria-label={getTranslation(translations, btn.Title)}>
<button class={"buttonicon " + btn.Icon} data-align={btn.DataAlign} aria-label={getTranslation(translations, btn.Title)}></button>
</a>
</li>
}
}
}

// 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, buttonGroups []pluginTypes.ToolbarButtonGroup) {
<div id="editbar" class="toolbar enabledtoolbar" style="display: flex;">
<div id="toolbar-overlay"></div>

Expand All @@ -21,6 +44,8 @@ templ EditBar(translations map[string]string) {
<li data-type="button" data-key="outdent"><a class="grouped-right" title={translations["pad.toolbar.unindent.title"]} aria-label={translations["pad.toolbar.unindent.title"]}><button class=" buttonicon buttonicon-outdent" title={translations["pad.toolbar.unindent.title"]} aria-label={translations["pad.toolbar.unindent.title"]}></button></a></li><li class="separator"></li><li data-type="button" data-key="undo"><a class="grouped-left" title={translations["pad.toolbar.undo.title"]} aria-label={translations["pad.toolbar.undo.title"]}><button class=" buttonicon buttonicon-undo" title={translations["pad.toolbar.undo.title"]} aria-label={translations["pad.toolbar.undo.title"]}></button></a></li>
<li data-type="button" data-key="redo"><a class="grouped-right" title={translations["pad.toolbar.redo.title"]} aria-label={translations["pad.toolbar.redo.title"]}><button class=" buttonicon buttonicon-redo" title={translations["pad.toolbar.redo.title"]} aria-label={translations["pad.toolbar.redo.title"]}></button></a></li><li class="separator"></li><li data-type="button" data-key="clearauthorship"><a class="" title={translations["pad.toolbar.clearAuthorship.title"]} aria-label={translations["pad.toolbar.clearAuthorship.title"]}><button class=" buttonicon buttonicon-clearauthorship" title={translations["pad.toolbar.clearAuthorship.title"]} aria-label={translations["pad.toolbar.clearAuthorship.title"]}></button></a></li>

@PluginToolbarButtons(translations, buttonGroups)

</ul>
<ul class="menu_right" role="toolbar">

Expand Down Expand Up @@ -323,7 +348,7 @@ templ ChatBox(translations map[string]string) {



templ PadIndex(pad padModel.Model, jsScript string, translations map[string]string, settings *settings.Settings, availableFonts []string) {
templ PadIndex(pad padModel.Model, jsScript string, translations map[string]string, settings *settings.Settings, availableFonts []string, buttonGroups []pluginTypes.ToolbarButtonGroup) {
<html class="pad super-light-toolbar super-light-editor light-background">
<head>
<title>{settings.Title}</title>
Expand All @@ -334,7 +359,7 @@ templ PadIndex(pad padModel.Model, jsScript string, translations map[string]stri
<link rel="localizations" type="application/l10n+json" href="/locales.json"/>
</head>
<body>
@EditBar(translations)
@EditBar(translations, buttonGroups)
<div id="editorcontainerbox" class="flex-layout">
<div id="editorcontainer" class="editorcontainer"></div>

Expand Down
1,777 changes: 990 additions & 787 deletions assets/pad/pad_templ.go

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions lib/api/io/exportHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions lib/api/static/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ func Init(store *lib.InitStore) {

store.C.Get("/pluginfw/plugin-definitions.json", plugins.ReturnPluginResponse)

store.C.Static("/static/plugins/", "./plugins")

adminHtml, err := getAdminBody(store.UiAssets, store.RetrievedSettings)

if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions lib/hooks/events/exportEvents.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions lib/hooks/events/getLineHTMLForExport.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions lib/hooks/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
85 changes: 70 additions & 15 deletions lib/io/exportDocx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -127,15 +147,33 @@ func (e *ExportDocx) generateDocumentXML(paragraphs []docxParagraph) string {
for _, para := range paragraphs {
bodyContent.WriteString("<w:p>")

// Add paragraph properties for lists
if para.listType != "" {
// Add paragraph properties for lists and alignment
if para.listType != "" || para.alignment != "" {
bodyContent.WriteString("<w:pPr>")
// 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(`<w:jc w:val="%s"/>`, 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(`<w:numPr><w:ilvl w:val="%d"/><w:numId w:val="%d"/></w:numPr>`, para.listLevel-1, numId))
}
bodyContent.WriteString(fmt.Sprintf(`<w:numPr><w:ilvl w:val="%d"/><w:numId w:val="%d"/></w:numPr>`, para.listLevel-1, numId))
bodyContent.WriteString("</w:pPr>")
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions lib/io/exportEtherpad.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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
Expand Down Expand Up @@ -209,15 +210,15 @@ 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 {
e.logger.Warnf("Failed to get docx document for id: %s with cause %s", id, err.Error())
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 {
Expand Down
Loading
Loading