Skip to content

Commit

Permalink
Add emoji plugin (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfcere authored May 18, 2020
1 parent 2c92dc2 commit 80fd0ef
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 24 deletions.
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@

# ngx-markdown

ngx-markdown is an [Angular](https://angular.io/) library that uses [marked](https://github.com/chjj/marked) to parse markdown to html combined with [Prism.js](http://prismjs.com/) for syntax highlight.
ngx-markdown is an [Angular](https://angular.io/) library that combines...
- [marked](http://marked.js.org/) to parse markdown to HTML
- [Prism.js](http://prismjs.com/) for language syntax highlight
- [Emoji-Toolkit](https://github.com/joypixels/emoji-toolkit) for emoji support
- [KaTeX](https://katex.org/) for math expression rendering

- Demo available @ [https://jfcere.github.io/ngx-markdown](https://jfcere.github.io/ngx-markdown)
- StackBlitz available @ [https://stackblitz.com/edit/ngx-markdown](https://stackblitz.com/edit/ngx-markdown)
Demo available @ [https://jfcere.github.io/ngx-markdown](https://jfcere.github.io/ngx-markdown)
StackBlitz available @ [https://stackblitz.com/edit/ngx-markdown](https://stackblitz.com/edit/ngx-markdown)

### Table of contents

Expand Down Expand Up @@ -153,6 +157,32 @@ Use `line` input property to specify the line(s) to highlight and optionally the
<markdown [src]="path/to/file.js" lineHighlight [line]="'6, 10-16'" [lineOffset]="5"></markdown>
```

### Emoji support

> :bell: Emoji support is **optional**, skip this step if you are not planning to use it
To activate [Emoji-Toolkit](https://github.com/joypixels/emoji-toolkit) for emoji suppport you will need to include...
- Emoji-Toolkit library - `node_modules/emoji-toolkit/lib/js/joypixels.min.js`

If you are using [Angular CLI](https://cli.angular.io/) you can follow the `angular.json` example below...

```diff
"scripts": [
"node_modules/marked/lib/marked.js",
+ "node_modules/emoji-toolkit/lib/js/joypixels.min.js",
]
```

#### Emoji plugin

Using `markdown` component and/or directive, you will be able to use the `emoji` property to activate [Emoji-Toolkit](https://github.com/joypixels/emoji-toolkit) plugin that converts emoji shortnames such as `:heart:` to native unicode emojis.

```html
<markdown emoji>I :heart: ngx-markdown</markdown>
```

> :blue_book: You can refer to this [Emoji Cheat Sheet](https://github.com/ikatyang/emoji-cheat-sheet/blob/master/README.md) for a complete list of _shortnames_.
### Math rendering

> :bell: Math rendering is **optional**, skip this step if you are not planning to use it
Expand Down Expand Up @@ -234,7 +264,7 @@ imports: [

#### Sanitization

As per ngx-markdown v9.0.0 **sanitization is enabled by default** and uses Angular `DomSanitizer` with `SecurityContext.HTML` to avoid XSS vulnerabilities. The `SecurityContext` level can be changed using the `sanitize` property when configuring `MarkdownModule`.
As of ngx-markdown v9.0.0 **sanitization is enabled by default** and uses Angular `DomSanitizer` with `SecurityContext.HTML` to avoid XSS vulnerabilities. The `SecurityContext` level can be changed using the `sanitize` property when configuring `MarkdownModule`.

```typescript
import { SecurityContext } from '@angular/core';
Expand Down Expand Up @@ -502,9 +532,8 @@ Building with AoT is part of the CI and is tested every time a commit occurs so
Here is the list of tasks that will be done on this library in the near future ...

- Update Marked to 1.0
- Add support for emojis
- Add copy-to-clipboard feature
- Support Prism.js customizing options (line-numbers, line-height, ...)
- Add a FAQ section to the README.md

## Contribution

Expand Down
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
],
"scripts": [
"node_modules/marked/lib/marked.js",
"node_modules/emoji-toolkit/lib/js/joypixels.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js",
Expand Down
29 changes: 29 additions & 0 deletions demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ <h1 class="title">
<li><a href="#variable-binding">Variable Binding</a></li>
<li><a href="#pipe">Pipe Usage</a></li>
<li class="menu-subtitle"><span>Plugins</span></li>
<li><a href="#emoji">Emoji</a></li>
<li><a href="#line-numbers">Line Numbers</a></li>
<li><a href="#line-highlight">Line Highlight</a></li>
<li><a href="#katex">KaTeX</a></li>
Expand Down Expand Up @@ -201,6 +202,34 @@ <h2 class="subtitle">Pipe Usage</h2>
<div class="pipe-usage" [innerHTML]="typescriptMarkdown | language : 'typescript' | markdown"></div>
</section>

<!-- PLUGINS - Emoji -->
<section id="emoji">
<h2 class="subtitle">Emoji plugin</h2>

<markdown ngPreserveWhitespaces>
#### Emoji-Toolkit file to include
```javascript
node_modules/emoji-toolkit/lib/js/joypixels.min.js
```
#### Directive
`emoji` - activate emoji plugin
### Example
</markdown>

<markdown>
Using `emoji` input property on `markdown` component or directive (this is not available when using pipe or service) allows you to convert shortnames to native unicode emojis.
</markdown>

<markdown [src]="'app/remote/emoji.html'"></markdown>

<markdown>
The example below illustrate `emoji` directive in action.
</markdown>

<textarea class="emoji-textarea" [(ngModel)]="emojiMarkdown"></textarea>
<markdown class="emoji-usage" [data]="emojiMarkdown" emoji></markdown>
</section>

<!-- PLUGINS - LineNumbers -->
<section id="line-numbers">
<h2 class="subtitle">Line Numbers plugin</h2>
Expand Down
8 changes: 8 additions & 0 deletions demo/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ section:first-of-type {
padding: 13px;
}

.emoji-usage,
.emoji-textarea,
.katex-usage,
.katex-textarea,
.pipe-usage,
Expand All @@ -143,6 +145,10 @@ section:first-of-type {
}
}

.emoji-textarea {
min-height: 80px;
}

.katex-textarea {
min-height: 220px;
}
Expand All @@ -152,6 +158,7 @@ section:first-of-type {
min-height: 320px;
}

.emoji-textarea,
.katex-textarea,
.pipe-textarea,
.variable-textarea {
Expand All @@ -160,6 +167,7 @@ section:first-of-type {
padding: 8px;
}

.emoji-usage,
.katex-usage,
.pipe-usage,
.variable-binding {
Expand Down
4 changes: 4 additions & 0 deletions demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class MarkdownDemoComponent {
}`;
//#endregion

//#region emoji
emojiMarkdown = `#### I :heart: ngx-markdown`;
//#endregion

//#region katex
katexMarkdown =
`#### \`katex\` directive example
Expand Down
1 change: 1 addition & 0 deletions demo/src/app/remote/emoji.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<markdown emoji>I :heart: ngx-markdown</markdown>
3 changes: 2 additions & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-markdown",
"version": "9.0.0",
"version": "9.1.0-beta.0",
"description": "Angular library that uses marked to parse markdown to html combined with Prism.js for synthax highlights",
"homepage": "https://github.com/jfcere/ngx-markdown",
"license": "MIT",
Expand All @@ -25,6 +25,7 @@
],
"dependencies": {
"@types/marked": "^0.7.2",
"emoji-toolkit": "^5.5.0",
"katex": "^0.11.0",
"marked": "^0.8.0",
"prismjs": "^1.16.0"
Expand Down
32 changes: 24 additions & 8 deletions lib/src/markdown.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ describe('MarkdownComponent', () => {
const raw = '### Raw';
const decoded = '<h3>Compiled</h3>';

spyOn(markdownService, 'compile').and.callFake((markdown: string, decodeHtml: boolean) => {
spyOn(markdownService, 'compile').and.callFake((markdown: string, decodeHtml: boolean, emojify: boolean) => {
return decodeHtml ? decoded : null;
});

component.render(raw, true);

expect(markdownService.compile).toHaveBeenCalledWith(raw, true);
expect(markdownService.compile).toHaveBeenCalledWith(raw, true, false);
expect(component.element.nativeElement.innerHTML).toBe(decoded);
});

Expand All @@ -193,28 +193,28 @@ describe('MarkdownComponent', () => {
const raw = '### Raw';
const undecoded = '<h3>Compiled-Undecoded</h3>';

spyOn(markdownService, 'compile').and.callFake((markdown: string, decodeHtml: boolean) => {
spyOn(markdownService, 'compile').and.callFake((markdown: string, decodeHtml: boolean, emojify: boolean) => {
return decodeHtml ? null : undecoded;
});

component.render(raw);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false);
expect(markdownService.compile).toHaveBeenCalledWith(raw, false, false);
expect(component.element.nativeElement.innerHTML).toBe(undecoded);

component.render(raw, false);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false);
expect(markdownService.compile).toHaveBeenCalledWith(raw, false, false);
expect(component.element.nativeElement.innerHTML).toBe(undecoded);

component.render(raw, null);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false);
expect(markdownService.compile).toHaveBeenCalledWith(raw, false, false);
expect(component.element.nativeElement.innerHTML).toBe(undecoded);

component.render(raw, undefined);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false);
expect(markdownService.compile).toHaveBeenCalledWith(raw, false, false);
expect(component.element.nativeElement.innerHTML).toBe(undecoded);
});

Expand All @@ -227,7 +227,7 @@ describe('MarkdownComponent', () => {

component.render(raw);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false);
expect(markdownService.compile).toHaveBeenCalledWith(raw, false, false);
expect(component.element.nativeElement.innerHTML).toBe(compiled);
});

Expand Down Expand Up @@ -276,6 +276,22 @@ describe('MarkdownComponent', () => {
expect(getHTMLPreElement().attributes.getNamedItem('data-line-offset').value).toBe('5');
});

it('should apply emoji plugin correctly', () => {

const raw = 'I :heart: ngx-markdown';
const emojified = 'I ❤️ ngx-markdown';

spyOn(markdownService, 'compile').and.callFake((markdown: string, decodeHtml: boolean, emojify: boolean) => {
return emojify ? emojified : null;
});

component.emoji = true;
component.render(raw);

expect(markdownService.compile).toHaveBeenCalledWith(raw, false, true);
expect(component.element.nativeElement.innerHTML).toBe(emojified);
});

it('should apply katex plugin correctly', () => {

const markdown = '$E=mc^2$';
Expand Down
9 changes: 8 additions & 1 deletion lib/src/markdown.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ export class MarkdownComponent implements OnChanges, AfterViewInit {
@Input() line: string | string[];
@Input() lineOffset: number;

// Plugin - emoji
@Input()
get emoji(): boolean { return this._emoji; }
set emoji(value: boolean) { this._emoji = this.coerceBooleanProperty(value); }

// Event emitters
@Output() error = new EventEmitter<string>();
@Output() load = new EventEmitter<string>();
@Output() ready = new EventEmitter<void>();

private _emoji = false;
private _katex = false;
private _lineHighlight = false;
private _lineNumbers = false;
Expand Down Expand Up @@ -63,7 +70,7 @@ export class MarkdownComponent implements OnChanges, AfterViewInit {
}

render(markdown: string, decodeHtml = false) {
let compiled = this.markdownService.compile(markdown, decodeHtml);
let compiled = this.markdownService.compile(markdown, decodeHtml, this.emoji);
compiled = this.katex ? this.markdownService.renderKatex(compiled, this.katexOptions) : compiled;
this.element.nativeElement.innerHTML = compiled;
this.handlePlugins();
Expand Down
48 changes: 47 additions & 1 deletion lib/src/markdown.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { parse } from 'marked';

import { KatexOptions } from './katex-options';
import { MarkdownModule } from './markdown.module';
import { errorKatexNotLoaded, MarkdownService, SECURITY_CONTEXT } from './markdown.service';
import { errorJoyPixelsNotLoaded, errorKatexNotLoaded, MarkdownService, SECURITY_CONTEXT } from './markdown.service';

declare var Prism: any;
declare var joypixels: any;
declare var katex: any;

describe('MarkdowService', () => {
Expand Down Expand Up @@ -198,6 +199,51 @@ describe('MarkdowService', () => {
expect(markdownService.compile(mockRaw, false)).toBe(expected);
expect(markdownService.compile(mockRaw, false)).toBe(expected);
});

it('should throw when emojify is true but emoji-toolkit is not loaded', () => {

global['joypixels'] = undefined;

expect(() => markdownService.compile('I :heart: ngx-markdown', false, true)).toThrowError(errorJoyPixelsNotLoaded);

global['joypixels'] = { shortnameToUnicode: undefined };

expect(() => markdownService.compile('I :heart: ngx-markdown', false, true)).toThrowError(errorJoyPixelsNotLoaded);
});

it('should call joypixels when emojify is true', () => {

const mockRaw = 'I :heart: ngx-markdown';
const mockEmojified = 'I ❤️ ngx-markdown';

global['joypixels'] = { shortnameToUnicode: () => {} };

spyOn(joypixels, 'shortnameToUnicode').and.returnValue(mockEmojified);

expect(markdownService.compile(mockRaw, false, true)).toEqual(parse(mockEmojified));
expect(joypixels.shortnameToUnicode).toHaveBeenCalledWith(mockRaw);
});

it('should not call joypixels when emojify is omitted/false/null/undefined', () => {

const mockRaw = '### Markdown-x';

global['joypixels'] = { shortnameToUnicode: () => {} };

spyOn(joypixels, 'shortnameToUnicode');

const useCases = [
() => markdownService.compile(mockRaw, false),
() => markdownService.compile(mockRaw, false, false),
() => markdownService.compile(mockRaw, false, null),
() => markdownService.compile(mockRaw, false, undefined),
];

useCases.forEach(func => {
func();
expect(joypixels.shortnameToUnicode).not.toHaveBeenCalled();
});
});
});

describe('getSource', () => {
Expand Down
Loading

0 comments on commit 80fd0ef

Please sign in to comment.