2021-10-06 21:03:29 +03:00
|
|
|
///
|
2022-01-17 14:07:46 +02:00
|
|
|
/// Copyright © 2016-2022 The Thingsboard Authors
|
2021-10-06 21:03:29 +03:00
|
|
|
///
|
|
|
|
|
/// 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 { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
|
|
|
|
|
import { Inject, Injectable } from '@angular/core';
|
|
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
|
import { DOCUMENT } from '@angular/common';
|
|
|
|
|
import { WINDOW } from '@core/services/window.service';
|
2022-07-07 13:20:33 +03:00
|
|
|
import { Tokenizer, marked } from 'marked';
|
2021-11-03 16:12:52 +02:00
|
|
|
import { Clipboard } from '@angular/cdk/clipboard';
|
2021-10-06 21:03:29 +03:00
|
|
|
|
|
|
|
|
const copyCodeBlock = '{:copy-code}';
|
2021-10-25 13:18:07 +03:00
|
|
|
const codeStyleRegex = '^{:code-style="(.*)"}\n';
|
2021-10-18 13:10:11 +03:00
|
|
|
const autoBlock = '{:auto}';
|
2021-10-07 16:09:20 +03:00
|
|
|
const targetBlankBlock = '{:target="_blank"}';
|
2021-10-06 21:03:29 +03:00
|
|
|
|
2021-10-07 10:26:01 +03:00
|
|
|
// @dynamic
|
2021-10-08 19:34:07 +03:00
|
|
|
@Injectable({
|
|
|
|
|
providedIn: 'root'
|
|
|
|
|
})
|
2021-10-06 21:03:29 +03:00
|
|
|
export class MarkedOptionsService extends MarkedOptions {
|
|
|
|
|
|
|
|
|
|
renderer = new MarkedRenderer();
|
|
|
|
|
headerIds = true;
|
|
|
|
|
gfm = true;
|
|
|
|
|
breaks = false;
|
|
|
|
|
pedantic = false;
|
|
|
|
|
smartLists = true;
|
|
|
|
|
smartypants = false;
|
2021-10-22 13:27:55 +03:00
|
|
|
mangle = false;
|
2021-10-06 21:03:29 +03:00
|
|
|
|
|
|
|
|
private renderer2 = new MarkedRenderer();
|
|
|
|
|
|
|
|
|
|
private id = 1;
|
|
|
|
|
|
|
|
|
|
constructor(private translate: TranslateService,
|
2021-11-03 16:12:52 +02:00
|
|
|
private clipboardService: Clipboard,
|
2021-10-06 21:03:29 +03:00
|
|
|
@Inject(WINDOW) private readonly window: Window,
|
|
|
|
|
@Inject(DOCUMENT) private readonly document: Document) {
|
|
|
|
|
super();
|
2021-10-22 13:27:55 +03:00
|
|
|
// @ts-ignore
|
|
|
|
|
const tokenizer: Tokenizer = {
|
|
|
|
|
autolink(src: string, mangle: (cap: string) => string): marked.Tokens.Link {
|
|
|
|
|
if (src.endsWith(copyCodeBlock)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
} else {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
url(src: string, mangle: (cap: string) => string): marked.Tokens.Link {
|
|
|
|
|
if (src.endsWith(copyCodeBlock)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
} else {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
marked.use({tokenizer});
|
2021-10-06 21:03:29 +03:00
|
|
|
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
|
2021-10-25 13:18:07 +03:00
|
|
|
const codeContext = processCode(code);
|
|
|
|
|
if (codeContext.copyCode) {
|
|
|
|
|
const content = postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext);
|
2021-10-06 21:03:29 +03:00
|
|
|
this.id++;
|
2021-10-25 13:18:07 +03:00
|
|
|
return this.wrapCopyCode(this.id, content, codeContext);
|
2021-10-06 21:03:29 +03:00
|
|
|
} else {
|
2021-10-25 13:18:07 +03:00
|
|
|
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext));
|
2021-10-06 21:03:29 +03:00
|
|
|
}
|
|
|
|
|
};
|
2021-10-18 13:10:11 +03:00
|
|
|
this.renderer.table = (header: string, body: string) => {
|
|
|
|
|
let autoLayout = false;
|
|
|
|
|
if (header.includes(autoBlock)) {
|
|
|
|
|
autoLayout = true;
|
|
|
|
|
header = header.replace(autoBlock, '');
|
|
|
|
|
}
|
|
|
|
|
let table = this.renderer2.table(header, body);
|
|
|
|
|
if (autoLayout) {
|
|
|
|
|
table = table.replace('<table', '<table class="auto"');
|
|
|
|
|
}
|
|
|
|
|
return table;
|
|
|
|
|
};
|
2021-10-06 21:03:29 +03:00
|
|
|
this.renderer.tablecell = (content: string, flags: {
|
|
|
|
|
header: boolean;
|
|
|
|
|
align: 'center' | 'left' | 'right' | null;
|
|
|
|
|
}) => {
|
2021-10-25 13:18:07 +03:00
|
|
|
const codeContext = processCode(content);
|
|
|
|
|
codeContext.multiline = false;
|
|
|
|
|
if (codeContext.copyCode) {
|
2021-10-06 21:03:29 +03:00
|
|
|
this.id++;
|
2021-10-25 13:18:07 +03:00
|
|
|
content = this.wrapCopyCode(this.id, codeContext.code, codeContext);
|
2021-10-06 21:03:29 +03:00
|
|
|
}
|
|
|
|
|
return this.renderer2.tablecell(content, flags);
|
|
|
|
|
};
|
2021-10-07 16:09:20 +03:00
|
|
|
this.renderer.link = (href: string | null, title: string | null, text: string) => {
|
|
|
|
|
if (text.endsWith(targetBlankBlock)) {
|
|
|
|
|
text = text.substring(0, text.length - targetBlankBlock.length);
|
|
|
|
|
const content = this.renderer2.link(href, title, text);
|
|
|
|
|
return content.replace('<a href=', '<a target="_blank" href=');
|
|
|
|
|
} else {
|
|
|
|
|
return this.renderer2.link(href, title, text);
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-10-06 21:03:29 +03:00
|
|
|
this.document.addEventListener('selectionchange', this.onSelectionChange.bind(this));
|
|
|
|
|
(this.window as any).markdownCopyCode = this.markdownCopyCode.bind(this);
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-07 16:09:20 +03:00
|
|
|
private wrapDiv(content: string): string {
|
|
|
|
|
return `<div>${content}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-25 13:18:07 +03:00
|
|
|
private wrapCopyCode(id: number, content: string, context: CodeContext): string {
|
|
|
|
|
let copyCodeButtonClass = 'clipboard-btn';
|
|
|
|
|
if (context.multiline) {
|
|
|
|
|
copyCodeButtonClass += ' multiline';
|
|
|
|
|
}
|
2021-10-06 21:03:29 +03:00
|
|
|
return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` +
|
2021-10-25 13:18:07 +03:00
|
|
|
`<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(context.code)}</span>` +
|
|
|
|
|
`<button class="${copyCodeButtonClass}">\n` +
|
2021-10-06 21:03:29 +03:00
|
|
|
` <p>${this.translate.instant('markdown.copy-code')}</p>\n` +
|
|
|
|
|
` <div>\n` +
|
|
|
|
|
` <img src="/assets/copy-code-icon.svg" alt="${this.translate.instant('markdown.copy-code')}">\n` +
|
|
|
|
|
` </div>\n` +
|
|
|
|
|
`</button>` +
|
|
|
|
|
`</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onSelectionChange() {
|
|
|
|
|
const codeWrappers = $('.code-wrapper');
|
|
|
|
|
codeWrappers.removeClass('noChars');
|
|
|
|
|
const selectedChars = this.getSelectedText();
|
|
|
|
|
if (!selectedChars) {
|
|
|
|
|
codeWrappers.addClass('noChars');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSelectedText(): string {
|
|
|
|
|
let text;
|
|
|
|
|
if (this.window.getSelection) {
|
|
|
|
|
text = this.window.getSelection().toString();
|
|
|
|
|
} else if (this.document.getSelection) {
|
|
|
|
|
text = this.document.getSelection();
|
|
|
|
|
} else if ((this.document as any).selection) {
|
|
|
|
|
text = (this.document as any).selection.createRange().text;
|
|
|
|
|
}
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private markdownCopyCode(id: number) {
|
|
|
|
|
const copyWrapper = $('#codeWrapper' + id);
|
|
|
|
|
if (copyWrapper.hasClass('noChars')) {
|
2021-10-13 20:35:10 +03:00
|
|
|
const text = decodeURIComponent($('#copyCodeId' + id).text());
|
2021-11-03 16:12:52 +02:00
|
|
|
if (this.clipboardService.copy(text)) {
|
2021-10-06 21:03:29 +03:00
|
|
|
import('tooltipster').then(
|
|
|
|
|
() => {
|
|
|
|
|
if (!copyWrapper.hasClass('tooltipstered')) {
|
|
|
|
|
copyWrapper.tooltipster(
|
|
|
|
|
{
|
|
|
|
|
content: this.translate.instant('markdown.copied'),
|
|
|
|
|
// theme: 'tooltipster-shadow',
|
|
|
|
|
delay: 0,
|
|
|
|
|
trigger: 'custom',
|
|
|
|
|
triggerClose: {
|
|
|
|
|
click: true,
|
|
|
|
|
tap: true,
|
|
|
|
|
scroll: true,
|
|
|
|
|
mouseleave: true
|
|
|
|
|
},
|
|
|
|
|
side: 'top',
|
|
|
|
|
distance: 12,
|
|
|
|
|
trackOrigin: true
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const tooltip = copyWrapper.tooltipster('instance');
|
|
|
|
|
tooltip.open();
|
2021-11-03 16:12:52 +02:00
|
|
|
});
|
|
|
|
|
}
|
2021-10-06 21:03:29 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-25 13:18:07 +03:00
|
|
|
interface CodeContext {
|
|
|
|
|
copyCode: boolean;
|
|
|
|
|
multiline: boolean;
|
|
|
|
|
codeStyle?: string;
|
|
|
|
|
code: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function processCode(code: string): CodeContext {
|
|
|
|
|
const context: CodeContext = {
|
|
|
|
|
copyCode: false,
|
|
|
|
|
multiline: false,
|
|
|
|
|
code
|
|
|
|
|
};
|
|
|
|
|
if (context.code.endsWith(copyCodeBlock)) {
|
|
|
|
|
context.code = context.code.substring(0, context.code.length - copyCodeBlock.length);
|
|
|
|
|
context.copyCode = true;
|
|
|
|
|
}
|
|
|
|
|
const codeStyleMatch = context.code.match(new RegExp(codeStyleRegex));
|
|
|
|
|
if (codeStyleMatch) {
|
|
|
|
|
context.codeStyle = codeStyleMatch[1];
|
|
|
|
|
context.code = context.code.replace(new RegExp(codeStyleRegex), '');
|
|
|
|
|
}
|
|
|
|
|
const lineCount = context.code.trim().split('\n').length;
|
|
|
|
|
context.multiline = lineCount > 1;
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function postProcessCodeContent(content: string, context: CodeContext): string {
|
|
|
|
|
let replacement = '<pre ngNonBindable';
|
|
|
|
|
if (!context.multiline) {
|
|
|
|
|
replacement += ' class="no-line-numbers"';
|
|
|
|
|
}
|
|
|
|
|
if (context.codeStyle) {
|
|
|
|
|
replacement += ` style="${context.codeStyle}"`;
|
2021-10-06 21:03:29 +03:00
|
|
|
}
|
2021-10-25 13:18:07 +03:00
|
|
|
replacement += '>';
|
2021-10-18 13:10:11 +03:00
|
|
|
return content.replace('<pre>', replacement);
|
2021-10-06 21:03:29 +03:00
|
|
|
}
|