diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index fa8d63be62..31cbeeb3b7 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -23,10 +23,12 @@ import { NgModule, NgModuleRef, OnDestroy, - Type + Type, + ɵresetCompiledComponents } from '@angular/core'; -import { Observable, ReplaySubject } from 'rxjs'; +import { from, Observable } from 'rxjs'; import { CommonModule } from '@angular/common'; +import { catchError, map, mergeMap } from 'rxjs/operators'; @NgModule() export abstract class DynamicComponentModule implements OnDestroy { @@ -56,10 +58,10 @@ export class DynamicComponentFactoryService { componentType: Type, template: string, modules?: Type[], - preserveWhitespaces?: boolean): Observable> { - const dymamicComponentFactorySubject = new ReplaySubject>(); - import('@angular/compiler').then( - () => { + preserveWhitespaces?: boolean, + compileAttempt = 1): Observable> { + return from(import('@angular/compiler')).pipe( + mergeMap(() => { const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); let moduleImports: Type[] = [CommonModule]; if (modules) { @@ -70,29 +72,33 @@ export class DynamicComponentFactoryService { declarations: [comp], imports: moduleImports })(class DynamicComponentInstanceModule extends DynamicComponentModule {}); - try { - this.compiler.compileModuleAsync(dynamicComponentInstanceModule).then( - (module) => { - const moduleRef = module.create(this.injector); - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); - this.dynamicComponentModulesMap.set(factory, { - moduleRef, - moduleType: module.moduleType - }); - dymamicComponentFactorySubject.next(factory); - dymamicComponentFactorySubject.complete(); + return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe( + map((module) => { + let moduleRef: NgModuleRef; + try { + moduleRef = module.create(this.injector); + } catch (e) { + this.compiler.clearCacheFor(module.moduleType); + throw e; } - ).catch( - (e) => { - dymamicComponentFactorySubject.error(e); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); + this.dynamicComponentModulesMap.set(factory, { + moduleRef, + moduleType: module.moduleType + }); + return factory; + }), + catchError((error) => { + if (compileAttempt === 1) { + ɵresetCompiledComponents(); + return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt); + } else { + throw error; } - ); - } catch (e) { - dymamicComponentFactorySubject.error(e); - } - } + }) + ); + }) ); - return dymamicComponentFactorySubject.asObservable(); } public destroyDynamicComponentFactory(factory: ComponentFactory) { diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html deleted file mode 100644 index 18cb6a4219..0000000000 --- a/ui-ngx/src/app/shared/components/markdown.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/ui-ngx/src/app/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss deleted file mode 100644 index a7a68b2aaa..0000000000 --- a/ui-ngx/src/app/shared/components/markdown.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright © 2016-2021 The Thingsboard Authors - * - * 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. - */ -:host { - markdown { - display: none; - } -} diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index fb83aec97b..c9fac8d9a7 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, ComponentFactory, ComponentRef, @@ -22,38 +23,31 @@ import { Inject, Injector, Input, OnChanges, - OnDestroy, - OnInit, Output, - Renderer2, SimpleChanges, - Type, - ViewChild, + SimpleChanges, + Type, ViewChild, ViewContainerRef } from '@angular/core'; import { HelpService } from '@core/services/help.service'; -import { MarkdownComponent } from 'ngx-markdown'; +import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; @Component({ selector: 'tb-markdown', - templateUrl: './markdown.component.html', - styleUrls: ['./markdown.component.scss'] + template: '' + + '' + + '
' + + '{{error}}' + + '
' }) -export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { - - private markdownComponent: MarkdownComponent; +export class TbMarkdownComponent implements OnChanges { @ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef; - @ViewChild('markdownComponent', {static: false}) set content(content: MarkdownComponent) { - this.markdownComponent = content; - if (this.isMarkdownReady && this.markdownComponent) { - this.processMarkdownComponent(); - } - } - @Input() data: string | undefined; @Input() markdownClass: string | undefined; @@ -70,35 +64,25 @@ export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { isMarkdownReady = false; + error = null; + private tbMarkdownInstanceComponentRef: ComponentRef; private tbMarkdownInstanceComponentFactory: ComponentFactory; constructor(private help: HelpService, - private renderer: Renderer2, + private cd: ChangeDetectorRef, + public markdownService: MarkdownService, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService) {} - ngOnInit(): void { - } - - ngOnDestroy(): void { - } - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'data') { - if (this.data) { - this.isMarkdownReady = false; - } - } - } + if (this.data) { + this.render(this.data); } } - private processMarkdownComponent() { - let template = this.markdownComponent.element.nativeElement.innerHTML; + private render(markdown: string) { + let template = this.markdownService.compile(markdown, false); template = this.sanitizeCurlyBraces(template); let markdownClass = 'tb-markdown-view'; if (this.markdownClass) { @@ -106,7 +90,6 @@ export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { } template = `
${template}
`; this.markdownContainer.clear(); - this.markdownComponent = null; const parent = this; this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { @@ -124,13 +107,38 @@ export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { this.tbMarkdownInstanceComponentRef = this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); this.tbMarkdownInstanceComponentRef.instance.style = this.style; - } catch (e) { + this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); + this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); + this.error = null; + } catch (error) { + this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); this.destroyMarkdownInstanceResources(); } + this.cd.detectChanges(); + this.ready.emit(); + }, + (error) => { + this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); + this.destroyMarkdownInstanceResources(); + this.cd.detectChanges(); this.ready.emit(); }); } + private handlePlugins(element: HTMLElement): void { + if (this.lineNumbers) { + this.setPluginClass(element, PrismPlugin.LineNumbers); + } + } + + private setPluginClass(element: HTMLElement, plugin: string | string[]): void { + const preElements = element.querySelectorAll('pre'); + for (let i = 0; i < preElements.length; i++) { + const classes = plugin instanceof Array ? plugin : [plugin]; + preElements.item(i).classList.add(...classes); + } + } + private sanitizeCurlyBraces(template: string): string { return template.replace(/{/g, '{').replace(/}/g, '}'); } @@ -142,13 +150,4 @@ export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { } this.tbMarkdownInstanceComponentRef = null; } - - onMarkdownReady() { - if (this.data) { - this.isMarkdownReady = true; - if (this.markdownComponent) { - this.processMarkdownComponent(); - } - } - } } diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 36944dbf81..1dd50841a3 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -516,12 +516,12 @@ mat-label { } p, div { - padding-right: 32px; color: rgba(15, 22, 29, 0.8); line-height: 1.5em; } - p, div { + & > p, & > div { + padding-right: 32px; padding-left: 32px; }