Jaime Still
Jaime Still

Reputation: 1998

Sanitizing ng-content for code snippets

I have an Angular component that uses PrismJS for syntax highlighting code blocks. The component is as follows:

import { Component, AfterViewInit, Input, 
    ElementRef, ViewChild, ChangeDetectorRef } from '@angular/core';

declare var Prism: any;

@Component({
  selector: 'prism',
  template: `
  <div hidden="true" #rawContent>
    <ng-content></ng-content>
  </div>
  <section class="code-container">
    <pre><code [innerHtml]="content" class="block language-{{language}}"></code></pre>
  </section>
  `
})
export class PrismComponent implements AfterViewInit {
  @Input() language: string;
  @ViewChild('rawContent') rawContent: ElementRef;
  content: string;

  constructor(public cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.content = Prism.highlight(this.rawContent.nativeElement.textContent.trim(),
        Prism.languages[this.language]);
    this.cdr.detectChanges();
  }
}

The issue is that when I use it, I have to manually escape any invalid HTML characters.

I've tried using DomSanitizer.sanitize() on both the component element reference value and the rawContent reference value in the following locations in order to try to circumvent this:

Angular throws invalid character errors before any of these events take place when the code block contains invalid HTML characters.

How would I go about sanitizing the code block specified in rawContent in order to prevent manually escaping invalid HTML characters?

Example StackBlitz Solution

Upvotes: 3

Views: 1065

Answers (2)

David
David

Reputation: 34445

As of angular 5.2, this is currently not possible because of the way single brackets are interpreted. Using ngNonBindable does not work with single brackets

https://github.com/angular/angular/issues/11859

This will fail

<div>
  Test content with a bracket {
</div> 

To include strings with backets in your template, you need to assign the string to a component's variable

//component.ts
 snippet = `export class Demo {
   id: number;
   name: string;
}`;

//component.html
<sanitized-prism [snippet]="snippet" language="typescript"></sanitized-prism>

SanitizePrismComponent

import {
  Component,
  AfterViewInit,
  Input,
  ChangeDetectorRef,
  SecurityContext
} from '@angular/core';

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

declare var Prism: any;

@Component({
  selector: 'sanitize-prism',
  template: `
  <section class="code-container">
    <pre><code [innerHtml]="content" class="block language-{{language}}"></code></pre>
  </section>
  `
})
export class SanitizePrismComponent implements AfterViewInit {
  @Input() snippet: string;
  @Input() language: string;
  content: SafeHtml;

  constructor(public cdr: ChangeDetectorRef, public sanitizer: DomSanitizer) { }

  ngAfterViewInit() {
    this.content = this.sanitizer.bypassSecurityTrustHtml(
      this.sanitizer.sanitize(
        SecurityContext.HTML, 
        Prism.highlight(this.snippet, Prism.languages[this.language])));
    this.cdr.detectChanges();
  }
}

If you really want to hard code strings with brackets in your template file, then you can use of these solutions:

Encode brackets

export class Demo &#123; id: number; name: string; &#125;

(which is what you already had)

Wrap everything in an interpolated expression

{{"export class Demo { id: number; name: string; } }"}

Note: if the string contains douoble quotes, you need to escape them with \"

{{"  let: str = \"string\"; "}}

Escape brackets (as suggested by angular's error message)

export class Demo {{'{' }} id: number; name: string; {{   '}'}

Wrap your code in CDATA (@Juan Mellado's suggestion)

<prism language="typescript">
  <![CDATA[ export class Demo { id: number; name: string; } ]]>
</prism>

The last one if the cleanest way IMHO

Upvotes: 0

Juan Mellado
Juan Mellado

Reputation: 15113

Use CDATA (Character Data):

<prism language="typescript">
  <![CDATA[ export class Demo { id: number; name: string; } ]]>
</prism>

Upvotes: 2

Related Questions