diff --git a/assets/pad/pad.templ b/assets/pad/pad.templ index 5d4b314..0846f81 100644 --- a/assets/pad/pad.templ +++ b/assets/pad/pad.templ @@ -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 { +
  • + for _, btn := range group.Buttons { +
  • + + + +
  • + } + } +} + +// 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) {
    @@ -21,6 +44,8 @@ templ EditBar(translations map[string]string) {
  • + @PluginToolbarButtons(translations, buttonGroups) +
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -943,38 +1146,38 @@ func EditorLoadingBox(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var70 := templ.GetChildren(ctx) - if templ_7745c5c3_Var70 == nil { - templ_7745c5c3_Var70 = templ.NopComponent + templ_7745c5c3_Var83 := templ.GetChildren(ctx) + if templ_7745c5c3_Var83 == nil { + templ_7745c5c3_Var83 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var71 string - templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.permissionDenied"]) + var templ_7745c5c3_Var84 string + templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.permissionDenied"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 42, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 67, Col: 53} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "


    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "


    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var72 string - templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.loading"]) + var templ_7745c5c3_Var85 string + templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.loading"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 47, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 72, Col: 42} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -998,253 +1201,253 @@ func SettingsPopup(translations map[string]string, availablefonts []string) temp }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var73 := templ.GetChildren(ctx) - if templ_7745c5c3_Var73 == nil { - templ_7745c5c3_Var73 = templ.NopComponent + templ_7745c5c3_Var86 := templ.GetChildren(ctx) + if templ_7745c5c3_Var86 == nil { + templ_7745c5c3_Var86 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var74 string - templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.padSettings"]) + var templ_7745c5c3_Var87 string + templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.padSettings"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 61, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 86, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var75 string - templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.myView"]) + var templ_7745c5c3_Var88 string + templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.myView"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 62, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 87, Col: 54} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, valueInLoop := range availablefonts { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for key, valueInLoop := range hooks.AvailableLangs { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var89 string - templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.about"]) + var templ_7745c5c3_Var102 string + templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.about"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 117, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 142, Col: 53} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var90 string - templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.poweredBy"]) + var templ_7745c5c3_Var103 string + templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.settings.poweredBy"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 118, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 143, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, " Etherpad
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, " Etherpad") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1268,64 +1471,64 @@ func EmbedPopup(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var91 := templ.GetChildren(ctx) - if templ_7745c5c3_Var91 == nil { - templ_7745c5c3_Var91 = templ.NopComponent + templ_7745c5c3_Var104 := templ.GetChildren(ctx) + if templ_7745c5c3_Var104 == nil { + templ_7745c5c3_Var104 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var92 string - templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share"]) + var templ_7745c5c3_Var105 string + templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 125, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 150, Col: 44} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var94 string - templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share.link"]) + var templ_7745c5c3_Var107 string + templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share.link"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 131, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 156, Col: 53} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var95 string - templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share.emebdcode"]) + var templ_7745c5c3_Var108 string + templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.share.emebdcode"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 135, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 160, Col: 58} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1349,155 +1552,155 @@ func ImportExportPopup(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var96 := templ.GetChildren(ctx) - if templ_7745c5c3_Var96 == nil { - templ_7745c5c3_Var96 = templ.NopComponent + templ_7745c5c3_Var109 := templ.GetChildren(ctx) + if templ_7745c5c3_Var109 == nil { + templ_7745c5c3_Var109 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var97 string - templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.import_export"]) + var templ_7745c5c3_Var110 string + templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.import_export"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 144, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 169, Col: 65} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var98 string - templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.import"]) + var templ_7745c5c3_Var111 string + templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.import"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 146, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 171, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var99 string - templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.abiword.innerHTML"]) + var templ_7745c5c3_Var112 string + templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.abiword.innerHTML"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 147, Col: 122} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 172, Col: 122} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var100 string - templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.importSuccessful"]) + var templ_7745c5c3_Var113 string + templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.importSuccessful"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 154, Col: 125} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 179, Col: 125} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var101 string - templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.export"]) + var templ_7745c5c3_Var114 string + templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.export"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 164, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 189, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var102 string - templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.exportetherpad"]) + var templ_7745c5c3_Var115 string + templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.importExport.exportetherpad"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 166, Col: 151} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 191, Col: 151} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1521,493 +1724,493 @@ func ConnectivityPopup(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var108 := templ.GetChildren(ctx) - if templ_7745c5c3_Var108 == nil { - templ_7745c5c3_Var108 = templ.NopComponent + templ_7745c5c3_Var121 := templ.GetChildren(ctx) + if templ_7745c5c3_Var121 == nil { + templ_7745c5c3_Var121 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var109 string - templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.connected"]) + var templ_7745c5c3_Var122 string + templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.connected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 191, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 216, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var110 string - templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.reconnecting"]) + var templ_7745c5c3_Var123 string + templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.reconnecting"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 194, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 219, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "


    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "


    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var111 string - templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup"]) + var templ_7745c5c3_Var124 string + templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 200, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 225, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var112 string - templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup.explanation"]) + var templ_7745c5c3_Var125 string + templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 201, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 226, Col: 67} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var113 string - templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup.advice"]) + var templ_7745c5c3_Var126 string + templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.userdup.advice"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 202, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 227, Col: 78} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var115 string - templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.unauth"]) + var templ_7745c5c3_Var128 string + templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.unauth"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 206, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 231, Col: 54} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var116 string - templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.unauth.explanation"]) + var templ_7745c5c3_Var129 string + templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.unauth.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 207, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 232, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var118 string - templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var131 string + templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 211, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 236, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var119 string - templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.looping.explanation"]) + var templ_7745c5c3_Var132 string + templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.looping.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 212, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 237, Col: 67} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var120 string - templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.looping.cause"]) + var templ_7745c5c3_Var133 string + templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.looping.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 213, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 238, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var121 string - templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail"]) + var templ_7745c5c3_Var134 string + templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 216, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 241, Col: 62} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var122 string - templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail.explanation"]) + var templ_7745c5c3_Var135 string + templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 217, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 242, Col: 74} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var123 string - templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail.cause"]) + var templ_7745c5c3_Var136 string + templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.initsocketfail.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 218, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 243, Col: 67} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var124 string - templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var137 string + templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 221, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 246, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var125 string - templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.slowcommit.explanation"]) + var templ_7745c5c3_Var138 string + templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.slowcommit.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 222, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 247, Col: 70} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var126 string - templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.slowcommit.cause"]) + var templ_7745c5c3_Var139 string + templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.slowcommit.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 223, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 248, Col: 80} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var128 string - templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var141 string + templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 227, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 252, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var129 string - templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.badChangeset.explanation"]) + var templ_7745c5c3_Var142 string + templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.badChangeset.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 228, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 253, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var130 string - templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.badChangeset.cause"]) + var templ_7745c5c3_Var143 string + templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.badChangeset.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 229, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 254, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var132 string - templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var145 string + templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 233, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 258, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var133 string - templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.corruptPad.explanation"]) + var templ_7745c5c3_Var146 string + templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.corruptPad.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 234, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 259, Col: 70} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var134 string - templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.corruptPad.cause"]) + var templ_7745c5c3_Var147 string + templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.corruptPad.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 235, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 260, Col: 63} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var135 string - templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.deleted"]) + var templ_7745c5c3_Var148 string + templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.deleted"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 238, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 263, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var136 string - templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.deleted.explanation"]) + var templ_7745c5c3_Var149 string + templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.deleted.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 239, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 264, Col: 66} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var137 string - templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rateLimited"]) + var templ_7745c5c3_Var150 string + templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rateLimited"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 242, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 267, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var138 string - templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rateLimited.explanation"]) + var templ_7745c5c3_Var151 string + templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rateLimited.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 243, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 268, Col: 70} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var139 string - templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var152 string + templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 246, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 271, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var140 string - templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rejected.explanation"]) + var templ_7745c5c3_Var153 string + templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rejected.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 247, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 272, Col: 68} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var141 string - templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rejected.cause"]) + var templ_7745c5c3_Var154 string + templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.rejected.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 248, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 273, Col: 61} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var142 string - templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) + var templ_7745c5c3_Var155 string + templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 251, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 276, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var143 string - templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected.explanation"]) + var templ_7745c5c3_Var156 string + templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected.explanation"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 252, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 277, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var144 string - templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected.cause"]) + var templ_7745c5c3_Var157 string + templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.modals.disconnected.cause"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 253, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 278, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var144)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2031,51 +2234,51 @@ func UserPopup(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var146 := templ.GetChildren(ctx) - if templ_7745c5c3_Var146 == nil { - templ_7745c5c3_Var146 = templ.NopComponent + templ_7745c5c3_Var159 := templ.GetChildren(ctx) + if templ_7745c5c3_Var159 == nil { + templ_7745c5c3_Var159 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2099,38 +2302,38 @@ func ChatPopup(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var150 := templ.GetChildren(ctx) - if templ_7745c5c3_Var150 == nil { - templ_7745c5c3_Var150 = templ.NopComponent + templ_7745c5c3_Var163 := templ.GetChildren(ctx) + if templ_7745c5c3_Var163 == nil { + templ_7745c5c3_Var163 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var152 string - templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.chat"]) + var templ_7745c5c3_Var165 string + templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.chat"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 295, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 320, Col: 58} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, " 0
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, " 0") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2154,51 +2357,51 @@ func ChatBox(translations map[string]string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var153 := templ.GetChildren(ctx) - if templ_7745c5c3_Var153 == nil { - templ_7745c5c3_Var153 = templ.NopComponent + templ_7745c5c3_Var166 := templ.GetChildren(ctx) + if templ_7745c5c3_Var166 == nil { + templ_7745c5c3_Var166 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var154 string - templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.chat"]) + var templ_7745c5c3_Var167 string + templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(translations["pad.chat"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 307, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 332, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "

    █  

    █  
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2206,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 { @@ -2222,59 +2425,59 @@ func PadIndex(pad padModel.Model, jsScript string, translations map[string]strin }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var157 := templ.GetChildren(ctx) - if templ_7745c5c3_Var157 == nil { - templ_7745c5c3_Var157 = templ.NopComponent + templ_7745c5c3_Var170 := templ.GetChildren(ctx) + if templ_7745c5c3_Var170 == nil { + templ_7745c5c3_Var170 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<html class=\"pad super-light-toolbar super-light-editor light-background\"><head><title>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var158 string - templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(settings.Title) + var templ_7745c5c3_Var171 string + templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(settings.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 329, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `assets/pad/pad.templ`, Line: 354, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "\" rel=\"stylesheet\">") 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 } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2298,7 +2501,7 @@ func PadIndex(pad padModel.Model, jsScript string, translations map[string]strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2314,7 +2517,7 @@ func PadIndex(pad padModel.Model, jsScript string, translations map[string]strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "
    ") 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/api/static/init.go b/lib/api/static/init.go index 81c1e74..fdd1ebe 100644 --- a/lib/api/static/init.go +++ b/lib/api/static/init.go @@ -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 { 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..ca2dc84 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, } } @@ -43,7 +47,10 @@ func (e *ExportHtml) GetPadHTMLDocument(padId string, revNum *int, readOnlyId *s return "", err } - htmlContent, err := e.GetPadHTML(retrievedPad, revNum, nil) + // Build author color cache + authorColors := e.buildAuthorColorCache(&retrievedPad.Pool) + + htmlContent, err := e.GetPadHTML(retrievedPad, revNum, authorColors) if err != nil { return "", err } @@ -78,11 +85,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 +369,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 +394,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 +422,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 +436,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 +467,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, "
        ") } } @@ -594,6 +633,25 @@ func processSpaces(s string) string { return strings.Join(parts, "") } +// buildAuthorColorCache builds a cache of author IDs to their colors +func (e *ExportHtml) buildAuthorColorCache(padPool *apool.APool) map[string]string { + authorColors := make(map[string]string) + + for _, attr := range padPool.NumToAttrib { + if attr.Key == "author" && attr.Value != "" { + authorId := attr.Value + if _, exists := authorColors[authorId]; !exists { + // Try to get author color from database + if authorData, err := e.AuthorManager.GetAuthor(authorId); err == nil { + authorColors[authorId] = authorData.ColorId + } + } + } + } + + return authorColors +} + // Export returns a rendered HTML string (for interface compatibility) func (e *ExportHtml) Export(padId string, revNum *int) (string, error) { return e.GetPadHTMLDocument(padId, revNum, nil) diff --git a/lib/io/exportHtml_test.go b/lib/io/exportHtml_test.go index 4f0ab6a..91670bf 100644 --- a/lib/io/exportHtml_test.go +++ b/lib/io/exportHtml_test.go @@ -5,8 +5,11 @@ import ( "testing" "github.com/ether/etherpad-go/lib/apool" + "github.com/ether/etherpad-go/lib/hooks" ) +var hooksToUse = hooks.NewHook() + func TestProcessSpaces(t *testing.T) { testCases := []struct { name string @@ -335,7 +338,9 @@ func TestFilterList(t *testing.T) { } func TestGetHTMLFromAtext_SimpleText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -343,7 +348,7 @@ func TestGetHTMLFromAtext_SimpleText(t *testing.T) { Attribs: "|1+c", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -357,7 +362,9 @@ func TestGetHTMLFromAtext_SimpleText(t *testing.T) { } func TestGetHTMLFromAtext_BoldText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "bold", Value: "true"}, nil) @@ -366,7 +373,7 @@ func TestGetHTMLFromAtext_BoldText(t *testing.T) { Attribs: "*0|1+5", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -383,7 +390,9 @@ func TestGetHTMLFromAtext_BoldText(t *testing.T) { } func TestGetHTMLFromAtext_ItalicText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "italic", Value: "true"}, nil) @@ -392,7 +401,7 @@ func TestGetHTMLFromAtext_ItalicText(t *testing.T) { Attribs: "*0|1+7", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -406,7 +415,9 @@ func TestGetHTMLFromAtext_ItalicText(t *testing.T) { } func TestGetHTMLFromAtext_UnderlineText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "underline", Value: "true"}, nil) @@ -415,7 +426,7 @@ func TestGetHTMLFromAtext_UnderlineText(t *testing.T) { Attribs: "*0|1+b", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -429,7 +440,9 @@ func TestGetHTMLFromAtext_UnderlineText(t *testing.T) { } func TestGetHTMLFromAtext_StrikethroughText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "strikethrough", Value: "true"}, nil) @@ -438,7 +451,7 @@ func TestGetHTMLFromAtext_StrikethroughText(t *testing.T) { Attribs: "*0|1+e", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -452,7 +465,9 @@ func TestGetHTMLFromAtext_StrikethroughText(t *testing.T) { } func TestGetHTMLFromAtext_Heading1(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "heading1", Value: "true"}, nil) @@ -461,7 +476,7 @@ func TestGetHTMLFromAtext_Heading1(t *testing.T) { Attribs: "*0|1+8", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -475,7 +490,9 @@ func TestGetHTMLFromAtext_Heading1(t *testing.T) { } func TestGetHTMLFromAtext_Heading2(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "heading2", Value: "true"}, nil) @@ -484,7 +501,7 @@ func TestGetHTMLFromAtext_Heading2(t *testing.T) { Attribs: "*0|1+a", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -498,7 +515,9 @@ func TestGetHTMLFromAtext_Heading2(t *testing.T) { } func TestGetHTMLFromAtext_MultipleFormats(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "bold", Value: "true"}, nil) pool.PutAttrib(apool.Attribute{Key: "italic", Value: "true"}, nil) @@ -508,7 +527,7 @@ func TestGetHTMLFromAtext_MultipleFormats(t *testing.T) { Attribs: "*0*1|1+b", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -522,7 +541,9 @@ func TestGetHTMLFromAtext_MultipleFormats(t *testing.T) { } func TestGetHTMLFromAtext_MixedFormattedText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "bold", Value: "true"}, nil) @@ -532,7 +553,7 @@ func TestGetHTMLFromAtext_MixedFormattedText(t *testing.T) { Attribs: "+7*0+4+8|1+1", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -546,7 +567,9 @@ func TestGetHTMLFromAtext_MixedFormattedText(t *testing.T) { } func TestGetHTMLFromAtext_EmptyText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -554,7 +577,7 @@ func TestGetHTMLFromAtext_EmptyText(t *testing.T) { Attribs: "|1+1", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -565,7 +588,9 @@ func TestGetHTMLFromAtext_EmptyText(t *testing.T) { } func TestGetHTMLFromAtext_MultipleLines(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -573,7 +598,7 @@ func TestGetHTMLFromAtext_MultipleLines(t *testing.T) { Attribs: "|1+7|1+7", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -590,7 +615,9 @@ func TestGetHTMLFromAtext_MultipleLines(t *testing.T) { } func TestGetHTMLFromAtext_SpecialCharacters(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -598,7 +625,7 @@ func TestGetHTMLFromAtext_SpecialCharacters(t *testing.T) { Attribs: "|1+1e", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -613,7 +640,9 @@ func TestGetHTMLFromAtext_SpecialCharacters(t *testing.T) { } func TestGetHTMLFromAtext_WithURL(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -621,7 +650,7 @@ func TestGetHTMLFromAtext_WithURL(t *testing.T) { Attribs: "|1+1g", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -638,7 +667,9 @@ func TestGetHTMLFromAtext_WithURL(t *testing.T) { } func TestGetHTMLFromAtext_BulletList(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "list", Value: "bullet1"}, nil) @@ -647,7 +678,7 @@ func TestGetHTMLFromAtext_BulletList(t *testing.T) { Attribs: "*0|1+8", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -664,7 +695,9 @@ func TestGetHTMLFromAtext_BulletList(t *testing.T) { } func TestGetHTMLFromAtext_NumberedList(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "list", Value: "number1"}, nil) @@ -673,7 +706,7 @@ func TestGetHTMLFromAtext_NumberedList(t *testing.T) { Attribs: "*0|1+8", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -690,7 +723,9 @@ func TestGetHTMLFromAtext_NumberedList(t *testing.T) { } func TestGetHTMLFromAtext_WithAuthorColors(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "author", Value: "a.test123"}, nil) @@ -703,7 +738,7 @@ func TestGetHTMLFromAtext_WithAuthorColors(t *testing.T) { Attribs: "*0|1+c", } - result, err := exporter.getHTMLFromAtext(&pool, atext, authorColors) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, authorColors) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -720,7 +755,9 @@ func TestGetHTMLFromAtext_WithAuthorColors(t *testing.T) { } func TestGetHTMLFromAtext_UnicodeText(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() atext := apool.AText{ @@ -728,7 +765,7 @@ func TestGetHTMLFromAtext_UnicodeText(t *testing.T) { Attribs: "|1+c", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -745,7 +782,9 @@ func TestGetHTMLFromAtext_UnicodeText(t *testing.T) { } func TestGetHTMLFromAtext_BulletFollowedByNumber(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "list", Value: "bullet1"}, nil) pool.PutAttrib(apool.Attribute{Key: "list", Value: "number1"}, nil) @@ -756,7 +795,7 @@ func TestGetHTMLFromAtext_BulletFollowedByNumber(t *testing.T) { Attribs: "*0|1+d*1|1+d", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -786,7 +825,9 @@ func TestGetHTMLFromAtext_BulletFollowedByNumber(t *testing.T) { } func TestGetHTMLFromAtext_MultipleBulletItems(t *testing.T) { - exporter := &ExportHtml{} + exporter := &ExportHtml{ + Hooks: &hooksToUse, + } pool := apool.NewAPool() pool.PutAttrib(apool.Attribute{Key: "list", Value: "bullet1"}, nil) @@ -796,7 +837,7 @@ func TestGetHTMLFromAtext_MultipleBulletItems(t *testing.T) { Attribs: "*0|1+8*0|1+8*0|1+8", } - result, err := exporter.getHTMLFromAtext(&pool, atext, nil) + result, err := exporter.getHTMLFromAtext("test-pad", &pool, atext, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } 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 a148af0..8a7e22c 100644 --- a/lib/plugins/PluginDef.go +++ b/lib/plugins/PluginDef.go @@ -1,10 +1,23 @@ package plugins +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 { - Name string `json:"name"` - Hooks map[string]string `json:"hooks"` - Plugin *string - FullName *string + Name string `json:"name"` + Hooks map[string]string `json:"hooks"` + ClientHooks map[string]string `json:"client_hooks"` + ToolbarButtons []ToolbarButton `json:"toolbar_buttons,omitempty"` + Plugin *string `json:"plugin"` + FullName *string `json:"full_name"` } type PluginDef 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 := "

        Hello World

        " + if *event.LineContent != expected { + t.Errorf("Expected '%s', got '%s'", expected, *event.LineContent) + } +} + +func TestGetLineHTMLForExport_HeadingWithAlignment(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 := "

        Hello World

        " + if *event.LineContent != expected { + t.Errorf("Expected '%s', got '%s'", expected, *event.LineContent) + } +} + +func TestGetLineHTMLForExport_RemovesLeadingAsterisk(t *testing.T) { + pool := createTestPool() + pool.PutAttrib(apool.Attribute{Key: "align", Value: "right"}, nil) + + lineContent := "*Some text" + text := "*Some text" + aline := "*0|1+1" + padId := "test-pad" + + event := &events.LineHtmlForExportContext{ + LineContent: &lineContent, + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + } + + GetLineHTMLForExport(event) + + // Should not contain the asterisk + expected := "

        Some text

        " + if *event.LineContent != expected { + t.Errorf("Expected '%s', got '%s'", expected, *event.LineContent) + } +} + +func TestGetLinePDFForExport_SetsAlignment(t *testing.T) { + pool := createTestPool() + pool.PutAttrib(apool.Attribute{Key: "align", Value: "center"}, nil) + + text := "*Hello World" + aline := "*0|1+1" + padId := "test-pad" + + event := &events.LinePDFForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLinePDFForExport(event) + + if event.Alignment == nil { + t.Fatal("Expected alignment to be set, got nil") + } + if *event.Alignment != "center" { + t.Errorf("Expected 'center' alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLinePDFForExport_NoAlignment(t *testing.T) { + pool := createTestPool() + + text := "Hello World" + aline := "" + padId := "test-pad" + + event := &events.LinePDFForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLinePDFForExport(event) + + if event.Alignment != nil { + t.Errorf("Expected nil alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLineDocxForExport_SetsAlignment(t *testing.T) { + pool := createTestPool() + pool.PutAttrib(apool.Attribute{Key: "align", Value: "right"}, nil) + + text := "*Hello World" + aline := "*0|1+1" + padId := "test-pad" + + event := &events.LineDocxForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLineDocxForExport(event) + + if event.Alignment == nil { + t.Fatal("Expected alignment to be set, got nil") + } + if *event.Alignment != "right" { + t.Errorf("Expected 'right' alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLineDocxForExport_NoAlignment(t *testing.T) { + pool := createTestPool() + + text := "Hello World" + aline := "" + padId := "test-pad" + + event := &events.LineDocxForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLineDocxForExport(event) + + if event.Alignment != nil { + t.Errorf("Expected nil alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLineOdtForExport_SetsAlignment(t *testing.T) { + pool := createTestPool() + pool.PutAttrib(apool.Attribute{Key: "align", Value: "justify"}, nil) + + text := "*Hello World" + aline := "*0|1+1" + padId := "test-pad" + + event := &events.LineOdtForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLineOdtForExport(event) + + if event.Alignment == nil { + t.Fatal("Expected alignment to be set, got nil") + } + if *event.Alignment != "justify" { + t.Errorf("Expected 'justify' alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLineOdtForExport_NoAlignment(t *testing.T) { + pool := createTestPool() + + text := "Hello World" + aline := "" + padId := "test-pad" + + event := &events.LineOdtForExportContext{ + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + Alignment: nil, + } + + GetLineOdtForExport(event) + + if event.Alignment != nil { + t.Errorf("Expected nil alignment, got '%s'", *event.Alignment) + } +} + +func TestGetLineHTMLForExport_AllAlignmentTypes(t *testing.T) { + alignments := []string{"left", "center", "right", "justify"} + + for _, alignment := range alignments { + t.Run(alignment, func(t *testing.T) { + pool := createTestPool() + pool.PutAttrib(apool.Attribute{Key: "align", Value: alignment}, nil) + + lineContent := "Test content" + text := "*Test content" + aline := "*0|1+1" + padId := "test-pad" + + event := &events.LineHtmlForExportContext{ + LineContent: &lineContent, + Apool: pool, + AttribLine: &aline, + Text: &text, + PadId: &padId, + } + + GetLineHTMLForExport(event) + + expected := "

        Test content

        " + if *event.LineContent != expected { + t.Errorf("Expected '%s', got '%s'", expected, *event.LineContent) + } + }) + } +} diff --git a/lib/plugins/ep_align/init.go b/lib/plugins/ep_align/init.go new file mode 100644 index 0000000..4f7396b --- /dev/null +++ b/lib/plugins/ep_align/init.go @@ -0,0 +1,32 @@ +package ep_align + +import ( + "github.com/ether/etherpad-go/lib/hooks" + "github.com/ether/etherpad-go/lib/hooks/events" +) + +func InitPlugin(hookSystem *hooks.Hook) { + // HTML Export hook + hookSystem.EnqueueHook("getLineHTMLForExport", func(hookName string, ctx any) { + var event = ctx.(*events.LineHtmlForExportContext) + GetLineHTMLForExport(event) + }) + + // PDF Export hook + hookSystem.EnqueueHook("getLinePDFForExport", func(hookName string, ctx any) { + var event = ctx.(*events.LinePDFForExportContext) + GetLinePDFForExport(event) + }) + + // DOCX Export hook + hookSystem.EnqueueHook("getLineDocxForExport", func(hookName string, ctx any) { + var event = ctx.(*events.LineDocxForExportContext) + GetLineDocxForExport(event) + }) + + // ODT Export hook + hookSystem.EnqueueHook("getLineOdtForExport", func(hookName string, ctx any) { + var event = ctx.(*events.LineOdtForExportContext) + GetLineOdtForExport(event) + }) +} diff --git a/lib/plugins/pluginInit.go b/lib/plugins/pluginInit.go new file mode 100644 index 0000000..c7779b7 --- /dev/null +++ b/lib/plugins/pluginInit.go @@ -0,0 +1,15 @@ +package plugins + +import ( + "github.com/ether/etherpad-go/lib/hooks" + "github.com/ether/etherpad-go/lib/plugins/ep_align" + "github.com/ether/etherpad-go/lib/settings" +) + +func InitPlugins(s *settings.Settings, hookSystem *hooks.Hook) { + if _, ok := s.Plugins["ep_align"]; ok { + if s.Plugins["ep_align"].Enabled { + ep_align.InitPlugin(hookSystem) + } + } +} diff --git a/lib/plugins/plugins.go b/lib/plugins/plugins.go index a32ca92..0cab73d 100644 --- a/lib/plugins/plugins.go +++ b/lib/plugins/plugins.go @@ -2,9 +2,11 @@ package plugins import ( "encoding/json" - "github.com/gofiber/fiber/v2" "os" "path" + + "github.com/ether/etherpad-go/lib/settings" + "github.com/gofiber/fiber/v2" ) type Plugin struct { @@ -14,21 +16,65 @@ type Plugin struct { RealPath string } +const corePluginName = "ep_etherpad-lite" + +// GetPackages gibt alle installierten Plugins zurück func GetPackages() map[string]Plugin { var mappedPlugins = make(map[string]Plugin) root, _ := os.Getwd() - mappedPlugins["ep_etherpad-lite"] = Plugin{ - Name: "ep_etherpad-lite", + mappedPlugins[corePluginName] = Plugin{ + Name: corePluginName, Version: "1.8.13", - Path: "node_modules/ep_etherpad-lite", + Path: "node_modules/" + corePluginName, RealPath: path.Join(root, "assets", "ep.json"), } + pluginsDir := path.Join(root, "plugins") + entries, err := os.ReadDir(pluginsDir) + if err == nil { + for _, entry := range entries { + if entry.IsDir() { + pluginName := entry.Name() + pluginPath := path.Join(pluginsDir, pluginName) + epJsonPath := path.Join(pluginPath, "ep.json") + if _, err := os.Stat(epJsonPath); err == nil { + mappedPlugins[pluginName] = Plugin{ + Name: pluginName, + Version: "0.0.1", // Standardversion, falls keine package.json vorhanden + Path: "node_modules/" + pluginName, + RealPath: epJsonPath, + } + } + } + } + } + return mappedPlugins } +// GetEnabledPackages gibt nur die in den Settings aktivierten Plugins zurück +func GetEnabledPackages() map[string]Plugin { + allPackages := GetPackages() + enabledPackages := make(map[string]Plugin) + + for name, plugin := range allPackages { + // ep_etherpad-lite is always activated + if name == corePluginName { + enabledPackages[name] = plugin + continue + } + + // Prüfe, ob das Plugin in den Settings aktiviert ist + if settings.Displayed.IsPluginEnabled(name) { + enabledPackages[name] = plugin + } + } + + return enabledPackages +} + func Update() (map[string]Plugin, map[string]Part, map[string]Plugin) { - var packages = GetPackages() + var packages = GetEnabledPackages() var parts = make(map[string]Part) var plugins = make(map[string]Plugin) @@ -65,16 +111,94 @@ func LoadPlugin(plugin Plugin, plugins map[string]Plugin, parts map[string]Part) type ClientPlugin struct { Plugins map[string]string `json:"plugins"` - Parts []string `json:"parts"` + Parts []Part `json:"parts"` } func ReturnPluginResponse(c *fiber.Ctx) error { + packages, parts, _ := Update() + var clientPlugins = ClientPlugin{ Plugins: map[string]string{}, - Parts: make([]string, 0), + Parts: make([]Part, 0), + } + + for _, pkg := range packages { + clientPlugins.Plugins[pkg.Name] = pkg.Version + } + + for _, part := range parts { + clientPlugins.Parts = append(clientPlugins.Parts, part) } var clPlugin, _ = json.Marshal(clientPlugins) c.GetRespHeaders()["Content-Type"] = []string{"application/json"} return c.Send(clPlugin) } + +// GetToolbarButtons gibt alle Toolbar-Buttons von aktivierten Plugins zurück +func GetToolbarButtons() []ToolbarButton { + _, parts, _ := Update() + var buttons []ToolbarButton + + for _, part := range parts { + if len(part.ToolbarButtons) > 0 { + buttons = append(buttons, part.ToolbarButtons...) + } + } + + return buttons +} + +// GetToolbarButtonGroups gibt Toolbar-Buttons gruppiert nach Plugin zurück +func GetToolbarButtonGroups() []ToolbarButtonGroup { + _, parts, _ := Update() + var groups []ToolbarButtonGroup + + for _, part := range parts { + if len(part.ToolbarButtons) > 0 { + pluginName := "" + if part.Plugin != nil { + pluginName = *part.Plugin + } + groups = append(groups, ToolbarButtonGroup{ + PluginName: pluginName, + Buttons: part.ToolbarButtons, + }) + } + } + + return groups +} + +// Cached plugin data +var cachedPlugins = map[string]Plugin{} +var cachedParts = map[string]Part{} +var cachedPackages = map[string]Plugin{} + +func init() { + GetCachedPlugins() +} + +// GetCachedPlugins returns cached plugins, loading them if necessary +func GetCachedPlugins() map[string]Plugin { + if len(cachedPlugins) == 0 { + cachedPackages, cachedParts, cachedPlugins = Update() + } + return cachedPlugins +} + +// GetCachedParts returns cached parts, loading them if necessary +func GetCachedParts() map[string]Part { + if cachedParts == nil { + cachedPackages, cachedParts, cachedPlugins = Update() + } + return cachedParts +} + +// GetCachedPackages returns cached packages, loading them if necessary +func GetCachedPackages() map[string]Plugin { + if cachedPackages == nil { + cachedPackages, cachedParts, cachedPlugins = Update() + } + return cachedPackages +} diff --git a/lib/settings/Settings.go b/lib/settings/Settings.go index abe33b0..f893600 100644 --- a/lib/settings/Settings.go +++ b/lib/settings/Settings.go @@ -134,6 +134,11 @@ type SSLSettings struct { Ca []string `json:"ca"` } +// PluginSettings definiert die Einstellungen für einzelne Plugins +type PluginSettings struct { + Enabled bool `json:"enabled"` +} + type Settings struct { Root string SettingsFilename string `json:"settingsFilename"` @@ -193,6 +198,18 @@ type Settings struct { DevMode bool `json:"devMode"` GitVersion string `json:"-"` AvailableExports []string `json:"availableExports"` + Plugins map[string]PluginSettings `json:"plugins"` +} + +// IsPluginEnabled prüft, ob ein Plugin in den Settings aktiviert ist +func (s *Settings) IsPluginEnabled(pluginName string) bool { + if s.Plugins == nil { + return false + } + if plugin, exists := s.Plugins[pluginName]; exists { + return plugin.Enabled + } + return false } func (s *Settings) GetPublicSettings() PublicSettings { diff --git a/lib/settings/clientVars/clientVars.go b/lib/settings/clientVars/clientVars.go index 4810f40..ee4f3c9 100644 --- a/lib/settings/clientVars/clientVars.go +++ b/lib/settings/clientVars/clientVars.go @@ -10,6 +10,7 @@ import ( "github.com/ether/etherpad-go/lib/models/pad" "github.com/ether/etherpad-go/lib/models/ws" pad2 "github.com/ether/etherpad-go/lib/pad" + "github.com/ether/etherpad-go/lib/plugins" "github.com/ether/etherpad-go/lib/settings" "github.com/ether/etherpad-go/lib/utils" ) @@ -80,9 +81,9 @@ func (f *Factory) NewClientVars(pad pad.Pad, sessionInfo *ws.Session, apool apoo Parts: make(map[string]clientVars.PartInMessage), } - var plugins = utils.GetPlugins() - for s := range plugins { - var rawParts = utils.GetParts() + var loadedPlugins = plugins.GetCachedPlugins() + for s := range loadedPlugins { + var rawParts = plugins.GetCachedParts() var convertedParts = make([]clientVars.PartInMessage, 0) for part := range rawParts { if rawParts[part].Plugin != nil && *rawParts[part].Plugin == s { @@ -97,10 +98,10 @@ func (f *Factory) NewClientVars(pad pad.Pad, sessionInfo *ws.Session, apool apoo rootPlugin.Plugins[s] = clientVars.PluginInMessage{ Parts: convertedParts, Package: clientVars.PluginInMessagePackage{ - Name: plugins[s].Name, - Path: plugins[s].Path, - RealPath: plugins[s].RealPath, - Version: plugins[s].Version, + Name: loadedPlugins[s].Name, + Path: loadedPlugins[s].Path, + RealPath: loadedPlugins[s].RealPath, + Version: loadedPlugins[s].Version, }, } } diff --git a/lib/settings/viper.go b/lib/settings/viper.go index f76cdfd..e28fd4b 100644 --- a/lib/settings/viper.go +++ b/lib/settings/viper.go @@ -339,5 +339,11 @@ func ReadConfig(jsonStr string) (*Settings, error) { DevMode: viper.GetBool(DevMode), } + // Parse Plugins + pluginsMap := make(map[string]PluginSettings) + if err := viper.UnmarshalKey("plugins", &pluginsMap); err == nil { + s.Plugins = pluginsMap + } + return s, nil } diff --git a/lib/test/api/io/export_handler_test.go b/lib/test/api/io/export_handler_test.go index 0c146e2..eb34e05 100644 --- a/lib/test/api/io/export_handler_test.go +++ b/lib/test/api/io/export_handler_test.go @@ -601,7 +601,7 @@ func testExportPlainTextPadAsDocx(t *testing.T, tsStore testutils.TestDataStore) token := createPadWithPlainText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/docx", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/word", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -631,7 +631,7 @@ func testExportBoldTextPadAsDocx(t *testing.T, tsStore testutils.TestDataStore) token := createPadWithBoldText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/docx", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/word", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -654,7 +654,7 @@ func testExportItalicTextPadAsDocx(t *testing.T, tsStore testutils.TestDataStore token := createPadWithItalicText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/docx", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/word", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -677,7 +677,7 @@ func testExportIndentedTextPadAsDocx(t *testing.T, tsStore testutils.TestDataSto token := createPadWithIndentation(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/docx", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/word", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -700,7 +700,7 @@ func testExportMixedFormattingPadAsDocx(t *testing.T, tsStore testutils.TestData token := createPadWithMixedFormatting(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/docx", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/word", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -724,7 +724,7 @@ func testExportPlainTextPadAsOdt(t *testing.T, tsStore testutils.TestDataStore) token := createPadWithPlainText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/odt", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/open", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -754,7 +754,7 @@ func testExportBoldTextPadAsOdt(t *testing.T, tsStore testutils.TestDataStore) { token := createPadWithBoldText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/odt", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/open", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -777,7 +777,7 @@ func testExportItalicTextPadAsOdt(t *testing.T, tsStore testutils.TestDataStore) token := createPadWithItalicText(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/odt", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/open", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -800,7 +800,7 @@ func testExportIndentedTextPadAsOdt(t *testing.T, tsStore testutils.TestDataStor token := createPadWithIndentation(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/odt", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/open", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) @@ -823,7 +823,7 @@ func testExportMixedFormattingPadAsOdt(t *testing.T, tsStore testutils.TestDataS token := createPadWithMixedFormatting(t, tsStore, padId, testText) - req := httptest.NewRequest("GET", "/p/"+padId+"/export/odt", nil) + req := httptest.NewRequest("GET", "/p/"+padId+"/export/open", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) resp, err := app.Test(req, 5000) assert.NoError(t, err) diff --git a/lib/utils/dbBootstrap.go b/lib/utils/dbBootstrap.go index cd42542..ee2590a 100644 --- a/lib/utils/dbBootstrap.go +++ b/lib/utils/dbBootstrap.go @@ -5,15 +5,10 @@ import ( "strconv" "github.com/ether/etherpad-go/lib/db" - plugins2 "github.com/ether/etherpad-go/lib/plugins" "github.com/ether/etherpad-go/lib/settings" "go.uber.org/zap" ) -func init() { - GetPlugins() -} - var ColorPalette = []string{ "#ffc7c7", "#fff1c7", @@ -120,28 +115,3 @@ func GetDB(retrievedSettings settings.Settings, setupLogger *zap.SugaredLogger) } return nil, errors.New("unsupported database type") } - -var plugins = map[string]plugins2.Plugin{} -var parts = map[string]plugins2.Part{} -var packages = map[string]plugins2.Plugin{} - -func GetPlugins() map[string]plugins2.Plugin { - if len(plugins) == 0 { - packages, parts, plugins = plugins2.Update() - } - return plugins -} - -func GetParts() map[string]plugins2.Part { - if parts == nil { - packages, parts, plugins = plugins2.Update() - } - return parts -} - -func GetPackages() map[string]plugins2.Plugin { - if packages == nil { - packages, parts, plugins = plugins2.Update() - } - return packages -} diff --git a/main.go b/main.go index 57477c0..fb64432 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,11 @@ package main import ( - "context" "embed" "fmt" _ "fmt" "net/http" + "os" "time" _ "github.com/ether/etherpad-go/docs" @@ -14,6 +14,7 @@ import ( "github.com/ether/etherpad-go/lib/author" "github.com/ether/etherpad-go/lib/hooks" "github.com/ether/etherpad-go/lib/pad" + "github.com/ether/etherpad-go/lib/plugins" session2 "github.com/ether/etherpad-go/lib/session" settings2 "github.com/ether/etherpad-go/lib/settings" "github.com/ether/etherpad-go/lib/utils" @@ -22,34 +23,8 @@ import ( "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" - "github.com/gorilla/sessions" ) -var store *sessions.CookieStore - -func sessionMiddleware(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - sessionFromCookie, err := store.Get(r, "express_sid") - if err != nil { - println("Error getting sessionFromCookie", err) - http.SetCookie(w, &http.Cookie{Name: "express_sid", MaxAge: -1, Path: "/"}) - return - } - - if sessionFromCookie.IsNew { - http.SetCookie(w, &http.Cookie{Name: "express_sid", MaxAge: -1, Path: "/"}) - err := sessionFromCookie.Save(r, w) - if err != nil { - println("Error saving sessionFromCookie", err) - return - } - } - - r = r.WithContext(context.WithValue(r.Context(), "sessionFromCookie", sessionFromCookie)) - h(w, r) - } -} - //go:embed assets var uiAssets embed.FS @@ -72,6 +47,10 @@ func main() { validatorEvaluator := validator.New(validator.WithRequiredStructEnabled()) retrievedHooks := hooks.NewHook() + + // init plugins + plugins.InitPlugins(&settings, &retrievedHooks) + gitVersion := settings2.GetGitCommit(&settings) setupLogger.Info("Starting Etherpad Go...") setupLogger.Info("Report bugs at https://github.com/ether/etherpad-go/issues") @@ -173,7 +152,8 @@ func main() { setupLogger.Info("Starting Web UI on " + fiberString) err = app.Listen(fiberString) if err != nil { - return + setupLogger.Error("Error starting web UI: " + err.Error()) + os.Exit(1) } } diff --git a/plugins/ep_align/ep.json b/plugins/ep_align/ep.json new file mode 100644 index 0000000..9a34800 --- /dev/null +++ b/plugins/ep_align/ep.json @@ -0,0 +1,48 @@ +{ + "parts": [ + { + "name": "ep_align", + "client_hooks": { + "aceRegisterBlockElements": "ep_align/static/js/index:aceRegisterBlockElements", + "aceAttribsToClasses": "ep_align/static/js/index:aceAttribsToClasses", + "aceDomLineProcessLineAttributes": "ep_align/static/js/index:aceDomLineProcessLineAttributes", + "aceEditEvent": "ep_align/static/js/index:aceEditEvent", + "aceEditorCSS": "ep_align/static/js/index:aceEditorCSS", + "aceInitialized": "ep_align/static/js/index:aceInitialized", + "postAceInit": "ep_align/static/js/index:postAceInit", + "postToolbarInit": "ep_align/static/js/index:postToolbarInit" + }, + "toolbar_buttons": [ + { + "key": "alignLeft", + "title": "ep_align.toolbar.left.title", + "icon": "buttonicon-align-left ep_align_left", + "group": "left", + "data_align": "0" + }, + { + "key": "alignCenter", + "title": "ep_align.toolbar.center.title", + "icon": "buttonicon-align-center ep_align_center", + "group": "middle", + "data_align": "1" + }, + { + "key": "alignRight", + "title": "ep_align.toolbar.right.title", + "icon": "buttonicon-align-right ep_align_right", + "group": "middle", + "data_align": "3" + }, + { + "key": "alignJustify", + "title": "ep_align.toolbar.justify.title", + "icon": "buttonicon-align-justify ep_align_justify", + "group": "right", + "data_align": "2" + } + ] + } + ] +} + diff --git a/plugins/ep_align/locales/de.json b/plugins/ep_align/locales/de.json new file mode 100644 index 0000000..bb7cba8 --- /dev/null +++ b/plugins/ep_align/locales/de.json @@ -0,0 +1,7 @@ +{ + "ep_align.toolbar.left.title": "Linksbündig", + "ep_align.toolbar.center.title": "Zentriert", + "ep_align.toolbar.right.title": "Rechtsbündig", + "ep_align.toolbar.justify.title": "Blocksatz" +} + diff --git a/plugins/ep_align/locales/en.json b/plugins/ep_align/locales/en.json new file mode 100644 index 0000000..ce83668 --- /dev/null +++ b/plugins/ep_align/locales/en.json @@ -0,0 +1,7 @@ +{ + "ep_align.toolbar.left.title": "Align Left", + "ep_align.toolbar.center.title": "Align Center", + "ep_align.toolbar.right.title": "Align Right", + "ep_align.toolbar.justify.title": "Align Justify" +} + diff --git a/plugins/ep_align/static/css/align.css b/plugins/ep_align/static/css/align.css new file mode 100644 index 0000000..51427e7 --- /dev/null +++ b/plugins/ep_align/static/css/align.css @@ -0,0 +1,51 @@ +/** + * ep_align - CSS Styles for Text Alignment + */ + +/* Alignment Classes */ +.align-left { + text-align: left !important; +} + +.align-center { + text-align: center !important; +} + +.align-right { + text-align: right !important; +} + +.align-justify { + text-align: justify !important; +} + +/* Toolbar Button Icons */ +.buttonicon-alignLeft:before { + content: "\f036"; /* fa-align-left */ + font-family: 'fontawesome-etherpad'; +} + +.buttonicon-alignCenter:before { + content: "\f037"; /* fa-align-center */ + font-family: 'fontawesome-etherpad'; +} + +.buttonicon-alignRight:before { + content: "\f038"; /* fa-align-right */ + font-family: 'fontawesome-etherpad'; +} + +.buttonicon-alignJustify:before { + content: "\f039"; /* fa-align-justify */ + font-family: 'fontawesome-etherpad'; +} + +/* Active button state */ +.toolbar .align-left-active .buttonicon-alignLeft, +.toolbar .align-center-active .buttonicon-alignCenter, +.toolbar .align-right-active .buttonicon-alignRight, +.toolbar .align-justify-active .buttonicon-alignJustify { + background-color: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + diff --git a/plugins/ep_align/static/js/index.ts b/plugins/ep_align/static/js/index.ts new file mode 100644 index 0000000..d5005da --- /dev/null +++ b/plugins/ep_align/static/js/index.ts @@ -0,0 +1,234 @@ +/** + * ep_align - Text Alignment Plugin for Etherpad + * + * Ermöglicht Text-Ausrichtung: links, zentriert, rechts, Blocksatz + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// Types +interface AceContext { + ace: { + callWithAce: (callback: (ace: AceEditor) => void, action: string, flag: boolean) => void; + }; + rep: RepState; + documentAttributeManager: DocumentAttributeManager; + editorInfo: EditorInfo; +} + +interface AceEditor { + ace_doInsertAlign: (level: number) => void; + ace_focus: () => void; +} + +interface RepState { + selStart: [number, number] | null; + selEnd: [number, number] | null; +} + +interface DocumentAttributeManager { + getAttributeOnLine: (line: number, attr: string) => string | null; + setAttributeOnLine: (line: number, attr: string, value: string) => void; + removeAttributeOnLine: (line: number, attr: string) => void; +} + +interface EditorInfo { + ace_doInsertAlign?: (level: number) => void; +} + +interface CallStack { + type: string; + docTextChanged?: boolean; +} + +interface EditEventCall { + callstack: CallStack; + documentAttributeManager: DocumentAttributeManager; + rep: RepState; +} + +interface ToolbarContext { + toolbar: { + registerCommand: (name: string, callback: () => void) => void; + }; + ace: AceContext['ace']; +} + +interface AttribContext { + key: string; + value: string; +} + +interface LineContext { + cls: string; +} + +// All our tags are block elements, so we just return them. +const tags: string[] = ['left', 'center', 'justify', 'right']; + +const range = (start: number, end: number): number[] => { + const length = Math.abs(end - start) + 1; + const result: number[] = []; + for (let i = 0; i < length; i++) { + result.push(start + i); + } + return result; +}; + +// Returns the block elements for alignment +export const aceRegisterBlockElements = (): string[] => tags; + +// Bind the event handler to the toolbar buttons +export const postAceInit = (_hookName: string, context: AceContext): void => { + const $ = (globalThis as any).jQuery || (globalThis as any).$; + + $('body').on('click', '.ep_align', function (this: HTMLElement) { + const value = $(this).data('align'); + const intValue = parseInt(value, 10); + if (!isNaN(intValue)) { + context.ace.callWithAce((ace: AceEditor) => { + ace.ace_doInsertAlign(intValue); + }, 'insertalign', true); + } + }); +}; + +// On caret position change show the current align +export const aceEditEvent = (_hook: string, call: EditEventCall): false | ReturnType => { + // If it's not a click or a key event and the text hasn't changed then do nothing + const cs = call.callstack; + if (cs.type !== 'handleClick' && cs.type !== 'handleKeyEvent' && !cs.docTextChanged) { + return false; + } + // If it's an initial setup event then do nothing.. + if (cs.type === 'setBaseText' || cs.type === 'setup') return false; + + // It looks like we should check to see if this section has this attribute + return setTimeout(() => { + const attributeManager = call.documentAttributeManager; + const rep = call.rep; + const activeAttributes: Record = {}; + + if (!rep.selStart || !rep.selEnd) return; + + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + let totalNumberOfLines = 0; + + range(firstLine, lastLine + 1).forEach((line) => { + totalNumberOfLines++; + const attr = attributeManager.getAttributeOnLine(line, 'align'); + if (attr) { + if (activeAttributes[attr]) { + activeAttributes[attr].count++; + } else { + activeAttributes[attr] = { count: 1 }; + } + } + }); + + // Check which alignment is active on all lines + for (const key in activeAttributes) { + if (Object.prototype.hasOwnProperty.call(activeAttributes, key)) { + const attr = activeAttributes[key]; + if (attr.count === totalNumberOfLines) { + // All lines have the same alignment - could be used to highlight button + } + } + } + }, 250); +}; + +// Our align attribute will result in a align:left class +export const aceAttribsToClasses = (_hook: string, context: AttribContext): string[] | undefined => { + if (context.key === 'align') { + return [`align:${context.value}`]; + } + return undefined; +}; + +// Here we convert the class align:left into a tag +export const aceDomLineProcessLineAttributes = (_name: string, context: LineContext): Array<{ + preHtml: string; + postHtml: string; + processedMarker: boolean; +}> => { + const cls = context.cls; + const alignType = /(?:^| )align:([A-Za-z0-9]*)/.exec(cls); + let tagIndex: number | undefined; + if (alignType) tagIndex = tags.indexOf(alignType[1]); + if (tagIndex !== undefined && tagIndex >= 0) { + const tag = tags[tagIndex]; + const styles = + `width:100%;margin:0 auto;list-style-position:inside;display:block;text-align:${tag}`; + const modifier = { + preHtml: `<${tag} style="${styles}">`, + postHtml: ``, + processedMarker: true, + }; + return [modifier]; + } + return []; +}; + +/** + * Adds CSS to the editor for alignment styles + */ +export const aceEditorCSS = (): string[] => { + return ['ep_align/static/css/align.css']; +}; + +// Once ace is initialized, we set ace_doInsertAlign and bind it to the context +export const aceInitialized = (_hook: string, context: AceContext): void => { + // Passing a level >= 0 will set a alignment on the selected lines, level < 0 + // will remove it + const doInsertAlign = function (this: AceContext, level: number): void { + const rep = this.rep; + const documentAttributeManager = this.documentAttributeManager; + if (!(rep.selStart && rep.selEnd) || (level >= 0 && tags[level] === undefined)) { + return; + } + + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + range(firstLine, lastLine + 1).forEach((i) => { + if (level >= 0) { + documentAttributeManager.setAttributeOnLine(i, 'align', tags[level]); + } else { + documentAttributeManager.removeAttributeOnLine(i, 'align'); + } + }); + }; + + const editorInfo = context.editorInfo; + editorInfo.ace_doInsertAlign = doInsertAlign.bind(context); +}; + +const align = (context: ToolbarContext, alignment: number): void => { + context.ace.callWithAce((ace: AceEditor) => { + ace.ace_doInsertAlign(alignment); + ace.ace_focus(); + }, 'insertalign', true); +}; + +export const postToolbarInit = (_hookName: string, context: ToolbarContext): boolean => { + const editbar = context.toolbar; + + editbar.registerCommand('alignLeft', () => { + align(context, 0); + }); + + editbar.registerCommand('alignCenter', () => { + align(context, 1); + }); + + editbar.registerCommand('alignJustify', () => { + align(context, 2); + }); + + editbar.registerCommand('alignRight', () => { + align(context, 3); + }); + + return true; +}; diff --git a/plugins/plugin-definitions.json b/plugins/plugin-definitions.json index d55835d..1e7cfd7 100644 --- a/plugins/plugin-definitions.json +++ b/plugins/plugin-definitions.json @@ -1 +1,210 @@ -{"plugins":{"ep_etherpad-lite":{"parts":[{"name":"DB","hooks":{"shutdown":"ep_etherpad-lite/node/db/DB"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/DB"},{"name":"Minify","hooks":{"shutdown":"ep_etherpad-lite/node/utils/Minify"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/Minify"},{"name":"express","hooks":{"createServer":"ep_etherpad-lite/node/hooks/express","restartServer":"ep_etherpad-lite/node/hooks/express","shutdown":"ep_etherpad-lite/node/hooks/express"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/express"},{"name":"static","hooks":{"expressPreSession":"ep_etherpad-lite/node/hooks/express/static"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/static"},{"name":"stats","hooks":{"shutdown":"ep_etherpad-lite/node/stats"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/stats"},{"name":"i18n","hooks":{"expressPreSession":"ep_etherpad-lite/node/hooks/i18n"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/i18n"},{"name":"specialpages","hooks":{"expressCreateServer":"ep_etherpad-lite/node/hooks/express/specialpages","expressPreSession":"ep_etherpad-lite/node/hooks/express/specialpages"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/specialpages"},{"name":"oauth2","hooks":{"expressCreateServer":"ep_etherpad-lite/node/security/OAuth2Provider"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/oauth2"},{"name":"padurlsanitize","hooks":{"expressCreateServer":"ep_etherpad-lite/node/hooks/express/padurlsanitize"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/padurlsanitize"},{"name":"apicalls","hooks":{"expressPreSession":"ep_etherpad-lite/node/hooks/express/apicalls"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/apicalls"},{"name":"importexport","hooks":{"expressCreateServer":"ep_etherpad-lite/node/hooks/express/importexport"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/importexport"},{"name":"errorhandling","hooks":{"expressCreateServer":"ep_etherpad-lite/node/hooks/express/errorhandling"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/errorhandling"},{"name":"socketio","hooks":{"expressCloseServer":"ep_etherpad-lite/node/hooks/express/socketio","expressCreateServer":"ep_etherpad-lite/node/hooks/express/socketio","socketio":"ep_etherpad-lite/node/handler/PadMessageHandler"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/socketio"},{"name":"tests","hooks":{"expressPreSession":"ep_etherpad-lite/node/hooks/express/tests"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/tests"},{"name":"admin","hooks":{"expressCreateServer":"ep_etherpad-lite/node/hooks/express/admin"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/admin"},{"name":"adminplugins","hooks":{"socketio":"ep_etherpad-lite/node/hooks/express/adminplugins"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/adminplugins"},{"name":"adminsettings","hooks":{"socketio":"ep_etherpad-lite/node/hooks/express/adminsettings"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/adminsettings"},{"name":"openapi","hooks":{"expressPreSession":"ep_etherpad-lite/node/hooks/express/openapi"},"plugin":"ep_etherpad-lite","full_name":"ep_etherpad-lite/openapi"}]}}} \ No newline at end of file +{ + "plugins": { + "ep_etherpad-lite": "1.8.13", + "ep_align": "0.0.1" + }, + "parts": [ + { + "name": "DB", + "hooks": { + "shutdown": "ep_etherpad-lite/node/db/DB" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/DB" + }, + { + "name": "Minify", + "hooks": { + "shutdown": "ep_etherpad-lite/node/utils/Minify" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/Minify" + }, + { + "name": "express", + "hooks": { + "createServer": "ep_etherpad-lite/node/hooks/express", + "restartServer": "ep_etherpad-lite/node/hooks/express", + "shutdown": "ep_etherpad-lite/node/hooks/express" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/express" + }, + { + "name": "static", + "hooks": { + "expressPreSession": "ep_etherpad-lite/node/hooks/express/static" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/static" + }, + { + "name": "stats", + "hooks": { + "shutdown": "ep_etherpad-lite/node/stats" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/stats" + }, + { + "name": "i18n", + "hooks": { + "expressPreSession": "ep_etherpad-lite/node/hooks/i18n" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/i18n" + }, + { + "name": "specialpages", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages", + "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/specialpages" + }, + { + "name": "oauth2", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/security/OAuth2Provider" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/oauth2" + }, + { + "name": "padurlsanitize", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/padurlsanitize" + }, + { + "name": "apicalls", + "hooks": { + "expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/apicalls" + }, + { + "name": "importexport", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/importexport" + }, + { + "name": "errorhandling", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/errorhandling" + }, + { + "name": "socketio", + "hooks": { + "expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio", + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio", + "socketio": "ep_etherpad-lite/node/handler/PadMessageHandler" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/socketio" + }, + { + "name": "tests", + "hooks": { + "expressPreSession": "ep_etherpad-lite/node/hooks/express/tests" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/tests" + }, + { + "name": "admin", + "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/admin" + }, + { + "name": "adminplugins", + "hooks": { + "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/adminplugins" + }, + { + "name": "adminsettings", + "hooks": { + "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/adminsettings" + }, + { + "name": "openapi", + "hooks": { + "expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/openapi" + }, + { + "name": "ep_message_all", + "client_hooks": { + "handleClientMessage_shoutMessage": "ep_etherpad-lite/static/js/messageHandler" + }, + "plugin": "ep_etherpad-lite", + "full_name": "ep_etherpad-lite/ep_message_all" + }, + { + "name": "ep_align", + "client_hooks": { + "aceRegisterBlockElements": "ep_align/static/js/index:aceRegisterBlockElements", + "aceAttribsToClasses": "ep_align/static/js/index:aceAttribsToClasses", + "aceDomLineProcessLineAttributes": "ep_align/static/js/index:aceDomLineProcessLineAttributes", + "aceEditEvent": "ep_align/static/js/index:aceEditEvent", + "aceEditorCSS": "ep_align/static/js/index:aceEditorCSS", + "aceInitialized": "ep_align/static/js/index:aceInitialized", + "postAceInit": "ep_align/static/js/index:postAceInit", + "postToolbarInit": "ep_align/static/js/index:postToolbarInit" + }, + "toolbar_buttons": [ + { + "key": "alignLeft", + "title": "ep_align.toolbar.left.title", + "icon": "buttonicon-align-left ep_align_left", + "group": "left", + "data_align": "0" + }, + { + "key": "alignCenter", + "title": "ep_align.toolbar.center.title", + "icon": "buttonicon-align-center ep_align_center", + "group": "middle", + "data_align": "1" + }, + { + "key": "alignRight", + "title": "ep_align.toolbar.right.title", + "icon": "buttonicon-align-right ep_align_right", + "group": "middle", + "data_align": "3" + }, + { + "key": "alignJustify", + "title": "ep_align.toolbar.justify.title", + "icon": "buttonicon-align-justify ep_align_justify", + "group": "right", + "data_align": "2" + } + ], + "plugin": "ep_align", + "full_name": "ep_align/ep_align" + } + ] +} \ No newline at end of file diff --git a/settings.template.json b/settings.template.json index 0f4d13b..5252b6c 100644 --- a/settings.template.json +++ b/settings.template.json @@ -141,5 +141,10 @@ "redirect_uris": ["http://localhost:9001/"] } ] + }, + "plugins": { + "ep_align": { + "enabled": false + } } } \ No newline at end of file diff --git a/ui/build.js b/ui/build.js index 5ebacc7..0d8658f 100644 --- a/ui/build.js +++ b/ui/build.js @@ -1,20 +1,262 @@ import * as esbuild from 'esbuild'; import * as fs from "node:fs"; +import * as path from "node:path"; import {exec, execSync} from "node:child_process"; +// ======================================== +// Plugin Registry Generator +// ======================================== + +const PLUGINS_DIR = '../plugins'; +const ASSETS_EP_JSON = '../assets/ep.json'; +const REGISTRY_OUTPUT = './src/js/pluginfw/plugin_registry.ts'; + +/** + * Lädt die ep.json eines Plugins + */ +function loadPluginDef(pluginPath) { + try { + const content = fs.readFileSync(pluginPath, 'utf-8'); + return JSON.parse(content); + } catch (err) { + console.error(`Failed to load plugin definition from ${pluginPath}:`, err); + return null; + } +} + +/** + * Sammelt alle client_hooks Module aus den Plugin-Definitionen + */ +function collectClientHooksModules(parts) { + const modules = new Map(); + + for (const part of parts) { + if (part.client_hooks) { + for (const [hookName, hookPath] of Object.entries(part.client_hooks)) { + const modulePath = hookPath.split(':')[0]; + modules.set(modulePath, modulePath); + } + } + } + + return modules; +} + +/** + * Konvertiert einen Plugin-Modul-Pfad zu einem relativen Require-Pfad + * Der Pfad ist relativ zu src/js/pluginfw/ wo die plugin_registry.ts liegt + */ +function modulePathToRequirePath(modulePath) { + if (modulePath.startsWith('ep_etherpad-lite/static/js/')) { + const localPath = modulePath.replace('ep_etherpad-lite/static/js/', ''); + return `../${localPath}`; + } + + // Externe Plugins: ep_align/static/js/index -> ../../../../plugins/ep_align/static/js/index + // Von src/js/pluginfw/ aus: ../../../ geht zu ui/, dann ../plugins/ + const pluginMatch = modulePath.match(/^(ep_[^/]+)\/static\/js\/(.+)$/); + if (pluginMatch) { + const [, pluginName, jsPath] = pluginMatch; + return `../../../../plugins/${pluginName}/static/js/${jsPath}`; + } + + return null; +} + +/** + * Generiert die plugin_registry.ts Datei + */ +function generatePluginRegistry(modules) { + const builtinModules = []; + + for (const [modulePath] of modules) { + const requirePath = modulePathToRequirePath(modulePath); + if (requirePath) { + builtinModules.push(` '${modulePath}': require('${requirePath}'),`); + } + } + + return `// @ts-nocheck +'use strict'; + +/** + * Plugin Registry - Registriert alle client_hooks Module zur Build-Zeit + * + * AUTOMATISCH GENERIERT - NICHT MANUELL BEARBEITEN + * Generiert von: build.js + */ + +const pluginUtils = require('./shared'); + +// Mapping von Modul-Pfaden zu ihren Implementierungen +const builtinModules = { +${builtinModules.join('\n')} +}; + +/** + * Registriert alle eingebauten Plugin-Module + */ +const registerBuiltinPlugins = () => { + for (const [path, module] of Object.entries(builtinModules)) { + pluginUtils.registerPluginModule(path, module); + } +}; + +/** + * Gibt eine Map aller verfügbaren Module zurück + */ +const getModuleMap = () => { + const map = new Map(); + for (const [path, module] of Object.entries(builtinModules)) { + map.set(path, module); + } + return map; +}; + +// Automatisch beim Import registrieren +registerBuiltinPlugins(); + +exports.registerBuiltinPlugins = registerBuiltinPlugins; +exports.getModuleMap = getModuleMap; +exports.builtinModules = builtinModules; +`; +} + +/** + * Generiert die Plugin-Definitionen und Registry + */ +function buildPlugins() { + const allParts = []; + const allPlugins = new Map(); + const absWorkingDir = process.cwd(); + + // 1. Lade das Core-Plugin (ep_etherpad-lite) + const corePluginPath = path.resolve(absWorkingDir, ASSETS_EP_JSON); + const corePlugin = loadPluginDef(corePluginPath); + + if (corePlugin) { + allPlugins.set('ep_etherpad-lite', '1.8.13'); + for (const part of corePlugin.parts) { + part.plugin = 'ep_etherpad-lite'; + part.full_name = `ep_etherpad-lite/${part.name}`; + allParts.push(part); + } + } + + // 2. Lade externe Plugins aus dem plugins/-Verzeichnis + const pluginsDir = path.resolve(absWorkingDir, PLUGINS_DIR); + if (fs.existsSync(pluginsDir)) { + const entries = fs.readdirSync(pluginsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const pluginName = entry.name; + const epJsonPath = path.join(pluginsDir, pluginName, 'ep.json'); + + if (fs.existsSync(epJsonPath)) { + const pluginDef = loadPluginDef(epJsonPath); + if (pluginDef) { + allPlugins.set(pluginName, '0.0.1'); + for (const part of pluginDef.parts) { + part.plugin = pluginName; + part.full_name = `${pluginName}/${part.name}`; + allParts.push(part); + } + } + } + } + } + } + + // 3. Sammle alle client_hooks Module + const clientHooksModules = collectClientHooksModules(allParts); + + // 4. Generiere plugin_registry.ts + const registryContent = generatePluginRegistry(clientHooksModules); + const registryPath = path.resolve(absWorkingDir, REGISTRY_OUTPUT); + fs.writeFileSync(registryPath, registryContent); + console.log(`Generated: ${registryPath}`); + + // 5. Generiere plugin-definitions.json + const pluginDefs = { + plugins: Object.fromEntries(allPlugins), + parts: allParts, + }; + const pluginDefsPath = path.resolve(absWorkingDir, PLUGINS_DIR, 'plugin-definitions.json'); + fs.writeFileSync(pluginDefsPath, JSON.stringify(pluginDefs, null, 2)); + console.log(`Generated: ${pluginDefsPath}`); + + console.log(`Registered ${allPlugins.size} plugin(s) with ${allParts.length} part(s)`); + console.log(`Found ${clientHooksModules.size} client_hooks module(s)\n`); + + // Rückgabe für Alias-Generierung + return { allPlugins, clientHooksModules }; +} + +// Generiere Plugin-Definitionen vor dem Build +console.log('Building plugin definitions...'); +const { allPlugins, clientHooksModules } = buildPlugins(); + +// ======================================== +// esbuild Configuration +// ======================================== const relativePath = 'ep_etherpad-lite/static/js'; const moduleResolutionPath = "./src/js" +// Basis-Aliase const alias = { [`${relativePath}/ace2_inner`]: `${moduleResolutionPath}/ace2_inner`, [`${relativePath}/ace2_common`]: `${moduleResolutionPath}/ace2_common`, [`${relativePath}/pluginfw/client_plugins`]: `${moduleResolutionPath}/pluginfw/client_plugins`, + [`${relativePath}/pluginfw/plugin_defs`]: `${moduleResolutionPath}/pluginfw/plugin_defs`, + [`${relativePath}/pluginfw/hooks`]: `${moduleResolutionPath}/pluginfw/hooks`, + [`${relativePath}/pluginfw/shared`]: `${moduleResolutionPath}/pluginfw/shared`, [`${relativePath}/rjquery`]: `${moduleResolutionPath}/rjquery`, [`${relativePath}/nice-select`]: `${moduleResolutionPath}/vendors/nice-select`, + // Client hooks module mappings + [`${relativePath}/messageHandler`]: `${moduleResolutionPath}/messageHandler`, + [`${relativePath}/pad`]: `${moduleResolutionPath}/pad`, + [`${relativePath}/chat`]: `${moduleResolutionPath}/chat`, + [`${relativePath}/pad_editbar`]: `${moduleResolutionPath}/pad_editbar`, + [`${relativePath}/pad_impexp`]: `${moduleResolutionPath}/pad_impexp`, + [`${relativePath}/collab_client`]: `${moduleResolutionPath}/collab_client`, + [`${relativePath}/broadcast`]: `${moduleResolutionPath}/broadcast`, + [`${relativePath}/broadcast_slider`]: `${moduleResolutionPath}/broadcast_slider`, + [`${relativePath}/broadcast_revisions`]: `${moduleResolutionPath}/broadcast_revisions`, + [`${relativePath}/colorutils`]: `${moduleResolutionPath}/colorutils`, + [`${relativePath}/cssmanager`]: `${moduleResolutionPath}/cssmanager`, + [`${relativePath}/pad_utils`]: `${moduleResolutionPath}/pad_utils`, + [`${relativePath}/pad_cookie`]: `${moduleResolutionPath}/pad_cookie`, + [`${relativePath}/pad_editor`]: `${moduleResolutionPath}/pad_editor`, + [`${relativePath}/pad_userlist`]: `${moduleResolutionPath}/pad_userlist`, + [`${relativePath}/pad_modals`]: `${moduleResolutionPath}/pad_modals`, + [`${relativePath}/pad_savedrevs`]: `${moduleResolutionPath}/pad_savedrevs`, + [`${relativePath}/pad_connectionstatus`]: `${moduleResolutionPath}/pad_connectionstatus`, + [`${relativePath}/pad_automatic_reconnect`]: `${moduleResolutionPath}/pad_automatic_reconnect`, + [`${relativePath}/scroll`]: `${moduleResolutionPath}/scroll`, + [`${relativePath}/caretPosition`]: `${moduleResolutionPath}/caretPosition`, + [`${relativePath}/security`]: `${moduleResolutionPath}/security`, + [`${relativePath}/Changeset`]: `${moduleResolutionPath}/Changeset`, + [`${relativePath}/AttributePool`]: `${moduleResolutionPath}/AttributePool`, + [`${relativePath}/ace`]: `${moduleResolutionPath}/ace`, + [`${relativePath}/timeslider`]: `${moduleResolutionPath}/timeslider`, + [`${relativePath}/socketio`]: `${moduleResolutionPath}/socketio`, + [`${relativePath}/underscore`]: `${moduleResolutionPath}/underscore`, + [`${relativePath}/skin_variants`]: `${moduleResolutionPath}/skin_variants`, }; +// Dynamisch Aliase für externe Plugins hinzufügen +for (const [modulePath] of clientHooksModules) { + const pluginMatch = modulePath.match(/^(ep_[^/]+)\/static\/js\/(.+)$/); + if (pluginMatch && !modulePath.startsWith('ep_etherpad-lite/')) { + const [, pluginName, jsPath] = pluginMatch; + alias[modulePath] = `../plugins/${pluginName}/static/js/${jsPath}`; + console.log(`Added alias: ${modulePath} -> ../plugins/${pluginName}/static/js/${jsPath}`); + } +} + const absWorkingDir = process.cwd() const loaders = { diff --git a/ui/scripts/build-plugins.ts b/ui/scripts/build-plugins.ts new file mode 100644 index 0000000..3835c99 --- /dev/null +++ b/ui/scripts/build-plugins.ts @@ -0,0 +1,215 @@ +/** + * Plugin Build Script + * + * Dieses Script scannt das plugins/-Verzeichnis nach Plugin-Definitionen + * und generiert eine aktualisierte plugin_registry.ts mit allen client_hooks Modulen. + * + * Verwendung: node scripts/build-plugins.js + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const PLUGINS_DIR = '../plugins'; +const ASSETS_EP_JSON = '../assets/ep.json'; +const REGISTRY_OUTPUT = './src/js/pluginfw/plugin_registry.ts'; + +interface Part { + name: string; + hooks?: Record; + client_hooks?: Record; + plugin?: string; + full_name?: string; +} + +interface PluginDef { + parts: Part[]; +} + +interface ClientPluginDef { + plugins: Record; + parts: Part[]; +} + +/** + * Lädt die ep.json eines Plugins + */ +function loadPluginDef(pluginPath: string): PluginDef | null { + try { + const content = fs.readFileSync(pluginPath, 'utf-8'); + return JSON.parse(content); + } catch (err) { + console.error(`Failed to load plugin definition from ${pluginPath}:`, err); + return null; + } +} + +/** + * Sammelt alle client_hooks Module aus den Plugin-Definitionen + */ +function collectClientHooksModules(parts: Part[]): Map { + const modules = new Map(); + + for (const part of parts) { + if (part.client_hooks) { + for (const [, hookPath] of Object.entries(part.client_hooks)) { + const modulePath = hookPath.split(':')[0]; + modules.set(modulePath, modulePath); + } + } + } + + return modules; +} + +/** + * Konvertiert einen Plugin-Modul-Pfad zu einem relativen Require-Pfad + */ +function modulePathToRequirePath(modulePath: string): string { + // ep_etherpad-lite/static/js/messageHandler -> ../messageHandler + if (modulePath.startsWith('ep_etherpad-lite/static/js/')) { + const localPath = modulePath.replace('ep_etherpad-lite/static/js/', ''); + return `../${localPath}`; + } + + // Externe Plugins: ep_example/static/js/index -> ../../plugins/ep_example/static/js/index + // Diese werden dynamisch geladen, nicht gebündelt + return null; +} + +/** + * Generiert die plugin_registry.ts Datei + */ +function generatePluginRegistry(modules: Map): string { + const builtinModules: string[] = []; + + Array.from(modules.entries()).forEach(([modulePath]) => { + const requirePath = modulePathToRequirePath(modulePath); + if (requirePath) { + builtinModules.push(` '${modulePath}': require('${requirePath}'),`); + } + }); + + return `// @ts-nocheck +'use strict'; + +/** + * Plugin Registry - Registriert alle client_hooks Module zur Build-Zeit + * + * AUTOMATISCH GENERIERT - NICHT MANUELL BEARBEITEN + * Generiert von: scripts/build-plugins.js + */ + +const pluginUtils = require('./shared'); + +// Mapping von Modul-Pfaden zu ihren Implementierungen +// Dieser Block wird zur Build-Zeit aufgelöst +const builtinModules = { +${builtinModules.join('\n')} +}; + +/** + * Registriert alle eingebauten Plugin-Module + */ +const registerBuiltinPlugins = () => { + for (const [path, module] of Object.entries(builtinModules)) { + pluginUtils.registerPluginModule(path, module); + } +}; + +/** + * Gibt eine Map aller verfügbaren Module zurück + */ +const getModuleMap = () => { + const map = new Map(); + for (const [path, module] of Object.entries(builtinModules)) { + map.set(path, module); + } + return map; +}; + +// Automatisch beim Import registrieren +registerBuiltinPlugins(); + +exports.registerBuiltinPlugins = registerBuiltinPlugins; +exports.getModuleMap = getModuleMap; +exports.builtinModules = builtinModules; +`; +} + +/** + * Generiert die plugin-definitions.json Datei + */ +function generatePluginDefinitions(plugins: Map, parts: Part[]): ClientPluginDef { + return { + plugins: Object.fromEntries(plugins), + parts: parts, + }; +} + +/** + * Hauptfunktion + */ +function main() { + const allParts: Part[] = []; + const allPlugins = new Map(); + + // 1. Lade das Core-Plugin (ep_etherpad-lite) + const corePluginPath = path.resolve(__dirname, ASSETS_EP_JSON); + const corePlugin = loadPluginDef(corePluginPath); + + if (corePlugin) { + allPlugins.set('ep_etherpad-lite', '1.8.13'); + for (const part of corePlugin.parts) { + part.plugin = 'ep_etherpad-lite'; + part.full_name = `ep_etherpad-lite/${part.name}`; + allParts.push(part); + } + } + + // 2. Lade externe Plugins aus dem plugins/-Verzeichnis + const pluginsDir = path.resolve(__dirname, PLUGINS_DIR); + if (fs.existsSync(pluginsDir)) { + const entries = fs.readdirSync(pluginsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const pluginName = entry.name; + const epJsonPath = path.join(pluginsDir, pluginName, 'ep.json'); + + if (fs.existsSync(epJsonPath)) { + const pluginDef = loadPluginDef(epJsonPath); + if (pluginDef) { + allPlugins.set(pluginName, '0.0.1'); + for (const part of pluginDef.parts) { + part.plugin = pluginName; + part.full_name = `${pluginName}/${part.name}`; + allParts.push(part); + } + } + } + } + } + } + + // 3. Sammle alle client_hooks Module + const clientHooksModules = collectClientHooksModules(allParts); + + // 4. Generiere plugin_registry.ts + const registryContent = generatePluginRegistry(clientHooksModules); + const registryPath = path.resolve(__dirname, REGISTRY_OUTPUT); + fs.writeFileSync(registryPath, registryContent); + console.log(`Generated: ${registryPath}`); + + // 5. Generiere plugin-definitions.json + const pluginDefs = generatePluginDefinitions(allPlugins, allParts); + const pluginDefsPath = path.resolve(__dirname, PLUGINS_DIR, 'plugin-definitions.json'); + fs.writeFileSync(pluginDefsPath, JSON.stringify(pluginDefs, null, 2)); + console.log(`Generated: ${pluginDefsPath}`); + + console.log(`\nRegistered ${allPlugins.size} plugin(s) with ${allParts.length} part(s)`); + console.log(`Found ${clientHooksModules.size} client_hooks module(s)`); +} + +main(); + diff --git a/ui/src/js/messageHandler.ts b/ui/src/js/messageHandler.ts new file mode 100644 index 0000000..94db0e3 --- /dev/null +++ b/ui/src/js/messageHandler.ts @@ -0,0 +1,41 @@ +// @ts-nocheck +'use strict'; + +/** + * Handler for the shoutMessage client message. + * This is part of the ep_message_all functionality. + */ + +const chat = require('./chat'); + +/** + * Handles the shoutMessage event - displays a message to all users in the pad. + * @param hookName - The name of the hook being called + * @param context - The context object containing the message payload + */ +exports.handleClientMessage_shoutMessage = (hookName: string, context: any) => { + const { payload } = context; + + if (!payload) { + console.warn('shoutMessage received without payload'); + return; + } + + // Display the shout message - could be shown in chat or as a notification + if (payload.message) { + // Option 1: Show in chat + if (chat.chat && typeof chat.chat.addMessage === 'function') { + chat.chat.addMessage({ + text: payload.message, + userId: payload.userId || 'system', + time: Date.now(), + isShoutMessage: true, + }, true, false); + } + + // Option 2: Show as alert/notification (fallback) + console.log(`[Shout Message]: ${payload.message}`); + } +}; + + diff --git a/ui/src/js/pluginfw/client_plugins.ts b/ui/src/js/pluginfw/client_plugins.ts index 0688d12..81cd75c 100644 --- a/ui/src/js/pluginfw/client_plugins.ts +++ b/ui/src/js/pluginfw/client_plugins.ts @@ -6,13 +6,68 @@ const defs = require('./plugin_defs'); exports.baseURL = ''; -exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb(); +exports.ensure = (cb) => !defs.loaded ? exports.update().then(cb) : cb(); + +// Lädt ein Plugin-Script dynamisch (nur für externe Plugins die nicht gebündelt sind) +const loadPluginScript = (pluginPath) => { + return new Promise((resolve, reject) => { + // Prüfe erst, ob das Modul bereits registriert ist (gebündelt) + if (pluginUtils.isModuleRegistered && pluginUtils.isModuleRegistered(pluginPath)) { + resolve(); + return; + } + + // Konvertiere Plugin-Pfad zu URL + // z.B. "ep_align/static/js/index" -> "/static/plugins/ep_align/static/js/index.js" + const url = `${exports.baseURL}static/plugins/${pluginPath}.js?v=${clientVars.randomVersionString}`; + + const script = document.createElement('script'); + script.src = url; + script.type = 'text/javascript'; + script.onload = () => { + // Das Plugin sollte sich selbst über window registriert haben + const moduleName = pluginPath; + if (window[moduleName]) { + pluginUtils.registerPluginModule(pluginPath, window[moduleName]); + } + resolve(); + }; + script.onerror = (err) => { + console.warn(`Failed to load plugin script: ${url}`, err); + resolve(); // Nicht ablehnen, damit andere Plugins geladen werden können + }; + document.head.appendChild(script); + }); +}; + +// Extrahiert alle Plugin-Pfade aus den Parts +const getPluginPaths = (parts) => { + const paths = new Set(); + for (const part of parts) { + if (part.client_hooks) { + for (const hookFnPath of Object.values(part.client_hooks)) { + // Extrahiere den Modul-Pfad (ohne Funktionsnamen nach dem Doppelpunkt) + const modulePath = hookFnPath.split(':')[0]; + // Ignoriere ep_etherpad-lite Pfade (sind im Build enthalten) + if (!modulePath.startsWith('ep_etherpad-lite/')) { + paths.add(modulePath); + } + } + } + } + return [...paths]; +}; exports.update = async (modules) => { const data = await jQuery.getJSON( `${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`); defs.plugins = data.plugins; defs.parts = data.parts; + + // Lade dynamisch die Plugin-Scripts + const pluginPaths = getPluginPaths(data.parts); + await Promise.all(pluginPaths.map(loadPluginScript)); + defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules); defs.loaded = true; }; diff --git a/ui/src/js/pluginfw/plugin_registry.ts b/ui/src/js/pluginfw/plugin_registry.ts new file mode 100644 index 0000000..453e6ca --- /dev/null +++ b/ui/src/js/pluginfw/plugin_registry.ts @@ -0,0 +1,44 @@ +// @ts-nocheck +'use strict'; + +/** + * Plugin Registry - Registriert alle client_hooks Module zur Build-Zeit + * + * AUTOMATISCH GENERIERT - NICHT MANUELL BEARBEITEN + * Generiert von: build.js + */ + +const pluginUtils = require('./shared'); + +// Mapping von Modul-Pfaden zu ihren Implementierungen +const builtinModules = { + 'ep_etherpad-lite/static/js/messageHandler': require('../messageHandler'), + 'ep_align/static/js/index': require('../../../../plugins/ep_align/static/js/index'), +}; + +/** + * Registriert alle eingebauten Plugin-Module + */ +const registerBuiltinPlugins = () => { + for (const [path, module] of Object.entries(builtinModules)) { + pluginUtils.registerPluginModule(path, module); + } +}; + +/** + * Gibt eine Map aller verfügbaren Module zurück + */ +const getModuleMap = () => { + const map = new Map(); + for (const [path, module] of Object.entries(builtinModules)) { + map.set(path, module); + } + return map; +}; + +// Automatisch beim Import registrieren +registerBuiltinPlugins(); + +exports.registerBuiltinPlugins = registerBuiltinPlugins; +exports.getModuleMap = getModuleMap; +exports.builtinModules = builtinModules; diff --git a/ui/src/js/pluginfw/shared.ts b/ui/src/js/pluginfw/shared.ts index a7c7617..414b463 100644 --- a/ui/src/js/pluginfw/shared.ts +++ b/ui/src/js/pluginfw/shared.ts @@ -10,6 +10,14 @@ const disabledHookReasons = { }, }; + +const loadedPluginModules = new Map(); + +// Prüft, ob ein Modul bereits registriert ist +exports.isModuleRegistered = (path) => { + return loadedPluginModules.has(path); +}; + const loadFn = (path, hookName, modules) => { let functionName; const parts = path.split(':'); @@ -25,11 +33,24 @@ const loadFn = (path, hookName, modules) => { functionName = parts[1]; } - let fn - if (modules === undefined || !("get" in modules)) { - fn = require(/* webpackIgnore: true */ path); - } else { + let fn; + if (modules !== undefined && "get" in modules) { fn = modules.get(path); + } else if (loadedPluginModules.has(path)) { + fn = loadedPluginModules.get(path); + } else { + // Versuche require (für Build-Zeit Module) + try { + fn = require(/* webpackIgnore: true */ path); + } catch (e) { + console.warn(`Could not require ${path}, may need to be loaded dynamically`); + return null; + } + } + + if (!fn) { + console.error(`[shared.loadFn] Module not found: ${path}`); + return null; } functionName = functionName ? functionName : hookName; @@ -37,9 +58,15 @@ const loadFn = (path, hookName, modules) => { for (const name of functionName.split('.')) { fn = fn[name]; } + return fn; }; +// Registriert ein Plugin-Modul für den späteren Zugriff +exports.registerPluginModule = (path, module) => { + loadedPluginModules.set(path, module); +}; + const extractHooks = (parts, hookSetName, normalizer, modules) => { const hooks = {}; for (const part of parts) { diff --git a/ui/src/pad.js b/ui/src/pad.js index f1a7f84..145feba 100644 --- a/ui/src/pad.js +++ b/ui/src/pad.js @@ -17,6 +17,10 @@ window.browser = require('./js/vendors/browser'); const pad = require('./js/pad'); pad.baseURL = basePath; + + // Plugin Registry laden - registriert alle eingebauten client_hooks Module + const pluginRegistry = require('./js/pluginfw/plugin_registry'); + window.plugins = require('./js/pluginfw/client_plugins'); const hooks = require('./js/pluginfw/hooks'); @@ -30,6 +34,10 @@ window.plugins.baseURL = basePath; + // Lade Plugin-Definitionen und extrahiere Hooks + // Übergebe die Module-Map damit die Hooks korrekt geladen werden + await window.plugins.update(pluginRegistry.getModuleMap()); + // Mechanism for tests to register hook functions (install fake plugins). window._postPluginUpdateForTestingDone = false; if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting(); diff --git a/ui/src/socketIoWrapper.ts b/ui/src/socketIoWrapper.ts index a68c105..04d4e93 100644 --- a/ui/src/socketIoWrapper.ts +++ b/ui/src/socketIoWrapper.ts @@ -84,7 +84,6 @@ export class SocketIoWrapper { private onMessage(evt: MessageEvent) { const arr = JSON.parse(evt.data) - console.log(`Received message: ${evt.data}`) if (!SocketIoWrapper.eventCallbacks[arr[0]]) return SocketIoWrapper.eventCallbacks[arr[0]].forEach(f => { f(arr[1]) diff --git a/ui/src/timeslider.js b/ui/src/timeslider.js index 822ef99..733a7bb 100644 --- a/ui/src/timeslider.js +++ b/ui/src/timeslider.js @@ -19,6 +19,8 @@ window.clientVars = { window.browser = require('./js/vendors/browser'); + const pluginRegistry = require('./js/pluginfw/plugin_registry'); + window.plugins = require('./js/pluginfw/client_plugins'); const socket = timeSlider.socket; BroadcastSlider = timeSlider.BroadcastSlider; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 75abdef..66c1119 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"],