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 { +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 := "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