From bfee935798179e3c872293adec2706768c634656 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 17 Jul 2026 10:00:33 +0400 Subject: [PATCH] feat(static-pages): rich text extensions + HTML-mode validation Milestone 3 of the Static Pages Module sprint. - MarketplaceHtmlEditorComponent toolbar: horizontal rule (insertHorizontalRule), code block (formatBlock -> PRE), embed (prompt for a URL, insert a sandboxed `, + ); } else { document.execCommand(item.command, false, item.value); } @@ -86,9 +105,19 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit toggleCode(): void { if (!this.showCode()) { this.codeValue.set(this.surface.nativeElement.innerHTML); + this.codeError.set(null); this.showCode.set(true); return; } + // Validate raw HTML before committing it back to the visual surface - + // malformed markup (an unclosed/mismatched tag) stays in code mode with + // an inline error rather than silently corrupting the visual editor. + const result = validateHtml(this.codeValue()); + if (!result.ok) { + this.codeError.set(result.error ?? this.translate.t('builder.htmlEditorInvalidHtml')); + return; + } + this.codeError.set(null); this.surface.nativeElement.innerHTML = this.codeValue(); this.showCode.set(false); this.emitChange(); @@ -96,6 +125,13 @@ export class MarketplaceHtmlEditorComponent implements OnChanges, AfterViewInit updateCode(value: string): void { this.codeValue.set(value); + if (this.codeError()) { + this.codeError.set(null); + } + } + + private escapeAttribute(value: string): string { + return value.replace(/&/g, '&').replace(/"/g, '"'); } private emitChange(): void { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 53629d0..a17d55a 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -551,6 +551,8 @@ export const en: Translations = { confirmLeaveUnsaved: 'You have unsaved changes. Leave anyway?', promptLinkUrl: 'URL', promptImageUrl: 'Image URL', + promptEmbedUrl: 'Embed URL', + htmlEditorInvalidHtml: 'This HTML has an unclosed or mismatched tag. Fix it before switching back to the visual editor.', htmlEditorCode: 'Code', htmlEditorPreview: 'Preview', lastSaved: 'Last saved', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 1e3f463..62c9eab 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -551,6 +551,8 @@ export const hy: Translations = { confirmLeaveUnsaved: 'Կան չպահված փոփոխություններ։ Այնուամենայնիվ դուրս գա՞լ։', promptLinkUrl: 'URL', promptImageUrl: 'Նկարի URL', + promptEmbedUrl: 'Ներդրման URL', + htmlEditorInvalidHtml: 'Այս HTML-ը ունի չփակված կամ չհամընկնող թեգ։ Ուղղեք նախքան տեսողական խմբագրիչին վերադառնալը։', htmlEditorCode: 'Կոդ', htmlEditorPreview: 'Նախադիտում', lastSaved: 'Վերջին պահպանումը', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 49f4744..201f08f 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -551,6 +551,8 @@ export const ru: Translations = { confirmLeaveUnsaved: 'Есть несохранённые изменения. Всё равно уйти?', promptLinkUrl: 'URL', promptImageUrl: 'URL изображения', + promptEmbedUrl: 'URL для встраивания', + htmlEditorInvalidHtml: 'В этом HTML есть незакрытый или несовпадающий тег. Исправьте перед возвратом к визуальному редактору.', htmlEditorCode: 'Код', htmlEditorPreview: 'Предпросмотр', lastSaved: 'Последнее сохранение', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 32fbb9b..33b933c 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -549,6 +549,8 @@ export interface Translations { confirmLeaveUnsaved: string; promptLinkUrl: string; promptImageUrl: string; + promptEmbedUrl: string; + htmlEditorInvalidHtml: string; htmlEditorCode: string; htmlEditorPreview: string; lastSaved: string;