From a4eb3b52cf3c10ef3d540f8d6d6b63eab4ff0126 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Thu, 2 Apr 2026 15:24:34 +0200 Subject: [PATCH 1/2] change incoming HTML to HTML4 --- .../productTranslationService.go | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/app/service/productTranslationService/productTranslationService.go b/app/service/productTranslationService/productTranslationService.go index 1b0a747..55f4f66 100644 --- a/app/service/productTranslationService/productTranslationService.go +++ b/app/service/productTranslationService/productTranslationService.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "os" + "regexp" "slices" "strings" "time" @@ -99,8 +100,9 @@ func (s *ProductTranslationService) SaveProductDescription(userID uint, productI // check that fields description, description_short and usage, if they exist, have a valid html format mustBeHTML := []string{"description", "description_short", "usage"} for i := 0; i < len(mustBeHTML); i++ { - if text, exists := updates[mustBeHTML[i]]; exists { - if !isValidXHTML(text) { + if _, exists := updates[mustBeHTML[i]]; exists { + updates[mustBeHTML[i]] = parseAutoCloseTags(updates[mustBeHTML[i]]) + if !isValidXHTML(updates[mustBeHTML[i]]) { return responseErrors.ErrInvalidXHTML } } @@ -245,9 +247,17 @@ func cleanForPrompt(s string) string { } } - prompt += ">" + if slices.Contains(xml.HTMLAutoClose, v.Name.Local) { + prompt += "/>" + } else { + prompt += ">" + } + case xml.EndElement: - prompt += "" + if !slices.Contains(xml.HTMLAutoClose, v.Name.Local) { + prompt += "" + } + case xml.CharData: prompt += string(v) case xml.Comment: @@ -288,6 +298,43 @@ func getStringInBetween(str string, start string, end string) (success bool, res return true, str[s : s+e] } +// this converts input into HTML4 format. +// this really is ad-hoc solution, but it works. +func parseAutoCloseTags(s string) string { + alts := "" + for i, name := range xml.HTMLAutoClose { + if i > 0 { + alts += "|" + } + alts += name + } + + // remove closing tags + reClose := regexp.MustCompile(`(?i)<\s*\/\s*(?:` + alts + `)\s*>`) + s = reClose.ReplaceAllString(s, "") + + // convert + // matches that do NOT already end with /> + reOpen := regexp.MustCompile(`(?i)<\s*(` + alts + `)\b([^>]*?)>`) + s = reOpen.ReplaceAllStringFunc(s, func(tag string) string { + trimmed := strings.TrimSpace(tag) + + // Already self-closed: ,
+ if strings.HasSuffix(trimmed, "/>") { + return tag + } + + // Replace final > with /> + i := strings.LastIndex(tag, ">") + if i < 0 { + return tag + } + return tag[:i] + " />" + }) + + return s +} + // isValidXHTML checks if the string obeys the XHTML format func isValidXHTML(s string) bool { r := strings.NewReader(s) @@ -363,7 +410,12 @@ func rebuildFromResponse(s_original string, s_response string) (bool, string) { result += fmt.Sprintf(` %s="%s"`, attrName(attr.Name), attr.Value) } } - result += ">" + + if slices.Contains(xml.HTMLAutoClose, v_original.Name.Local) { + result += "/>" + } else { + result += ">" + } case xml.CharData: result += string(v_response) @@ -381,7 +433,7 @@ func rebuildFromResponse(s_original string, s_response string) (bool, string) { return false, "" } - if v_original.Name.Local != "img" { + if !slices.Contains(xml.HTMLAutoClose, v_original.Name.Local) { result += "" } From c20cdf2b63e34fdb3cfb9f08c19c45d9360e4845 Mon Sep 17 00:00:00 2001 From: Arina Yakovenko Date: Thu, 2 Apr 2026 15:55:00 +0200 Subject: [PATCH 2/2] fix: editor --- bo/bun.lock | 30 +- bo/components.d.ts | 2 + bo/package.json | 1 + bo/src/App.vue | 5 +- bo/src/assets/main.css | 1 - bo/src/components/admin/PageProducts.vue | 6 +- bo/src/components/admin/ProductDetailView.vue | 391 ++++++++++++------ bo/src/layouts/default.vue | 11 +- bo/src/stores/product.ts | 37 -- 9 files changed, 299 insertions(+), 185 deletions(-) diff --git a/bo/bun.lock b/bo/bun.lock index 7481556..c09ac2e 100644 --- a/bo/bun.lock +++ b/bo/bun.lock @@ -1,25 +1,25 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "bo", "dependencies": { - "@nuxt/ui": "^4.5.0", - "@tailwindcss/vite": "^4.2.0", + "@nuxt/ui": "^4.6.0", + "@tailwindcss/vite": "^4.2.2", "chart.js": "^4.5.1", "pinia": "^3.0.4", - "tailwindcss": "^4.2.0", + "reka-ui": "^2.9.3", + "tailwindcss": "^4.2.2", "vue": "beta", "vue-chartjs": "^5.3.3", - "vue-i18n": "11", - "vue-router": "^5.0.2", + "vue-i18n": "^11.3.0", + "vue-router": "^5.0.4", }, "devDependencies": { "@tsconfig/node24": "^24.0.4", - "@types/node": "^24.10.13", - "@vitejs/plugin-vue": "^6.0.4", - "@vue/eslint-config-typescript": "^14.6.0", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "jiti": "^2.6.1", "npm-run-all2": "^8.0.4", @@ -27,8 +27,8 @@ "prettier": "3.8.1", "typescript": "~5.9.3", "vite": "beta", - "vite-plugin-vue-devtools": "^8.0.6", - "vue-tsc": "^3.2.4", + "vite-plugin-vue-devtools": "^8.1.1", + "vue-tsc": "^3.2.6", }, }, }, @@ -1012,7 +1012,7 @@ "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], - "reka-ui": ["reka-ui@2.9.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw=="], + "reka-ui": ["reka-ui@2.9.3", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-C9lCVxsSC7uYD0Nbgik1+14FNndHNprZmf0zGQt0ZDYIt5KxXV3zD0hEqNcfRUsEEJvVmoRsUkJnASBVBeaaUw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], @@ -1186,6 +1186,8 @@ "@nuxt/schema/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "@nuxt/ui/reka-ui": ["reka-ui@2.9.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw=="], + "@nuxtjs/color-mode/@nuxt/kit": ["@nuxt/kit@3.21.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g=="], "@nuxtjs/color-mode/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -1254,6 +1256,8 @@ "vaul-vue/@vueuse/core": ["@vueuse/core@10.11.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.1", "@vueuse/shared": "10.11.1", "vue-demi": ">=0.14.8" } }, "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww=="], + "vaul-vue/reka-ui": ["reka-ui@2.9.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw=="], + "vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], "vue-router/@vue/devtools-api": ["@vue/devtools-api@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1" } }, "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw=="], @@ -1283,5 +1287,7 @@ "vaul-vue/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@10.11.1", "", {}, "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw=="], "vaul-vue/@vueuse/core/@vueuse/shared": ["@vueuse/shared@10.11.1", "", { "dependencies": { "vue-demi": ">=0.14.8" } }, "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA=="], + + "vaul-vue/reka-ui/@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="], } } diff --git a/bo/components.d.ts b/bo/components.d.ts index 7c39661..d6d4dc7 100644 --- a/bo/components.d.ts +++ b/bo/components.d.ts @@ -46,6 +46,8 @@ declare module 'vue' { UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] UDrawer: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default'] UDropdownMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default'] + UEditor: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Editor.vue')['default'] + UEditorToolbar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/EditorToolbar.vue')['default'] UForm: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Form.vue')['default'] UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default'] UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default'] diff --git a/bo/package.json b/bo/package.json index b67b68d..5f80258 100644 --- a/bo/package.json +++ b/bo/package.json @@ -17,6 +17,7 @@ "@tailwindcss/vite": "^4.2.2", "chart.js": "^4.5.1", "pinia": "^3.0.4", + "reka-ui": "^2.9.3", "tailwindcss": "^4.2.2", "vue": "beta", "vue-chartjs": "^5.3.3", diff --git a/bo/src/App.vue b/bo/src/App.vue index df9f0b4..54265cd 100644 --- a/bo/src/App.vue +++ b/bo/src/App.vue @@ -1,10 +1,13 @@ diff --git a/bo/src/assets/main.css b/bo/src/assets/main.css index 13e3a47..6f09a8e 100644 --- a/bo/src/assets/main.css +++ b/bo/src/assets/main.css @@ -14,7 +14,6 @@ body { } @theme { - --main-light: #FFFEFB; --second-light: #F5F6FA; --main-dark: #212121; diff --git a/bo/src/components/admin/PageProducts.vue b/bo/src/components/admin/PageProducts.vue index 1df0d4c..c6c7efb 100644 --- a/bo/src/components/admin/PageProducts.vue +++ b/bo/src/components/admin/PageProducts.vue @@ -1,9 +1,11 @@