AAE-37906 Extract rich text parsing service (#11164)

* AAE-37906 Extract rich text parsing service

* AAE-37906 Add unit tests

* AAE-37906 Remove unused import

* AAE-37906 Fix units
This commit is contained in:
Wojciech Duda
2025-09-03 18:02:34 +02:00
committed by GitHub
parent 0564e67ba6
commit f1fea0a70b
4 changed files with 119 additions and 29 deletions

View File

@@ -18,12 +18,14 @@
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DisplayRichTextWidgetComponent } from './display-rich-text.widget'; import { DisplayRichTextWidgetComponent, RICH_TEXT_PARSER_TOKEN } from './display-rich-text.widget';
import { RichTextParserService } from '../../../services/rich-text-parser.service';
describe('DisplayRichTextWidgetComponent', () => { describe('DisplayRichTextWidgetComponent', () => {
let widget: DisplayRichTextWidgetComponent; let widget: DisplayRichTextWidgetComponent;
let fixture: ComponentFixture<DisplayRichTextWidgetComponent>; let fixture: ComponentFixture<DisplayRichTextWidgetComponent>;
let debugEl: DebugElement; let debugEl: DebugElement;
let mockRichTextParserService: jasmine.SpyObj<RichTextParserService>;
const cssSelector = { const cssSelector = {
parsedHTML: '.adf-display-rich-text-widget-parsed-html' parsedHTML: '.adf-display-rich-text-widget-parsed-html'
@@ -79,8 +81,14 @@ describe('DisplayRichTextWidgetComponent', () => {
}; };
beforeEach(() => { beforeEach(() => {
mockRichTextParserService = jasmine.createSpyObj('RichTextParserService', ['parse']);
mockRichTextParserService.parse.and.returnValue(
'<h1>Editor.js</h1><p class="ce-tune-alignment--left">Display some <font color="#ff1300">formatted</font> <mark class="cdx-marker">text</mark></p>'
);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [DisplayRichTextWidgetComponent] imports: [DisplayRichTextWidgetComponent],
providers: [{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService }]
}); });
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent); fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
widget = fixture.componentInstance; widget = fixture.componentInstance;
@@ -88,6 +96,13 @@ describe('DisplayRichTextWidgetComponent', () => {
widget.field = fakeFormField; widget.field = fakeFormField;
}); });
it('should call RichTextParserService.parse() method', () => {
fixture.detectChanges();
expect(mockRichTextParserService.parse).toHaveBeenCalledWith(fakeFormField.value);
expect(mockRichTextParserService.parse).toHaveBeenCalledTimes(1);
});
it('should parse editorjs data to html', async () => { it('should parse editorjs data to html', async () => {
const expectedHtml = const expectedHtml =
'<h1>Editor.js</h1><p class="ce-tune-alignment--left">Display some <font color="#ff1300">formatted</font> <mark class="cdx-marker">text</mark></p>'; '<h1>Editor.js</h1><p class="ce-tune-alignment--left">Display some <font color="#ff1300">formatted</font> <mark class="cdx-marker">text</mark></p>';

View File

@@ -17,10 +17,14 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core'; import { Component, inject, InjectionToken, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core';
import { WidgetComponent, FormService } from '@alfresco/adf-core'; import { WidgetComponent, FormService } from '@alfresco/adf-core';
import edjsHTML from 'editorjs-html';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { RichTextParserService } from '../../../services/rich-text-parser.service';
export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>('RichTextParserService', {
factory: () => new RichTextParserService()
});
@Component({ @Component({
selector: 'display-rich-text', selector: 'display-rich-text',
@@ -40,39 +44,22 @@ import { DomSanitizer } from '@angular/platform-browser';
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DisplayRichTextWidgetComponent extends WidgetComponent implements OnInit { export class DisplayRichTextWidgetComponent extends WidgetComponent implements OnInit {
parsedHTML: any; parsedHTML: string | Error;
private static readonly CUSTOM_PARSER = { private readonly richTextParserService = inject(RICH_TEXT_PARSER_TOKEN);
header: (block: any): string => { private readonly sanitizer = inject(DomSanitizer);
const paragraphAlign = block.data.alignment || block.data.align || block.tunes?.anyTuneName?.alignment;
if (typeof paragraphAlign !== 'undefined' && ['left', 'right', 'center'].includes(paragraphAlign)) {
return `<h${block.data.level} class="ce-tune-alignment--${paragraphAlign}">${block.data.text}</h${block.data.level}>`;
} else {
return `<h${block.data.level}>${block.data.text}</h${block.data.level}>`;
}
},
paragraph: (block: any): string => {
const paragraphAlign = block.data.alignment || block.data.align || block.tunes?.anyTuneName?.alignment;
if (typeof paragraphAlign !== 'undefined' && ['left', 'right', 'center', 'justify'].includes(paragraphAlign)) { constructor(formService: FormService) {
return `<p class="ce-tune-alignment--${paragraphAlign}">${block.data.text}</p>`;
} else {
return `<p>${block.data.text}</p>`;
}
}
};
constructor(public formService: FormService, private readonly sanitizer: DomSanitizer) {
super(formService); super(formService);
} }
ngOnInit(): void { ngOnInit(): void {
this.parsedHTML = edjsHTML(DisplayRichTextWidgetComponent.CUSTOM_PARSER, { strict: true }).parse(this.field.value); this.parsedHTML = this.richTextParserService.parse(this.field.value);
if (!(this.parsedHTML instanceof Error)) { if (this.parsedHTML instanceof Error) {
this.sanitizeHtmlContent();
} else {
throw this.parsedHTML; throw this.parsedHTML;
} else {
this.sanitizeHtmlContent();
} }
} }

View File

@@ -0,0 +1,32 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RichTextParserService } from './rich-text-parser.service';
describe('RichTextParserService', () => {
it('should test CUSTOM_PARSER.header method works correctly', () => {
const block = { data: { text: 'Test Header', level: 2, alignment: 'center' } };
const result = (RichTextParserService as any).CUSTOM_PARSER.header(block);
expect(result).toBe('<h2 class="ce-tune-alignment--center">Test Header</h2>');
});
it('should test CUSTOM_PARSER.paragraph method works correctly', () => {
const block = { data: { text: 'Test paragraph text', alignment: 'justify' } };
const result = (RichTextParserService as any).CUSTOM_PARSER.paragraph(block);
expect(result).toBe('<p class="ce-tune-alignment--justify">Test paragraph text</p>');
});
});

View File

@@ -0,0 +1,56 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import edjsHTML from 'editorjs-html';
export interface OutputData {
version?: string;
time?: number;
blocks: any[];
}
export class RichTextParserService {
private static readonly CUSTOM_PARSER = {
header: (block: any): string => {
if (!block.data || !block.data.text || !block.data.level) {
return '';
}
const paragraphAlign = block.data.alignment || block.data.align || block.tunes?.anyTuneName?.alignment;
if (typeof paragraphAlign !== 'undefined' && ['left', 'right', 'center'].includes(paragraphAlign)) {
return `<h${block.data.level} class="ce-tune-alignment--${paragraphAlign}">${block.data.text}</h${block.data.level}>`;
} else {
return `<h${block.data.level}>${block.data.text}</h${block.data.level}>`;
}
},
paragraph: (block: any): string => {
if (!block.data || !block.data.text) {
return '';
}
const paragraphAlign = block.data.alignment || block.data.align || block.tunes?.anyTuneName?.alignment;
if (typeof paragraphAlign !== 'undefined' && ['left', 'right', 'center', 'justify'].includes(paragraphAlign)) {
return `<p class="ce-tune-alignment--${paragraphAlign}">${block.data.text}</p>`;
} else {
return `<p>${block.data.text}</p>`;
}
}
};
parse(richText: OutputData): string | Error {
return edjsHTML(RichTextParserService.CUSTOM_PARSER, { strict: true }).parse(richText);
}
}