///
/// Copyright © 2016-2022 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.
///
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';
import { Tokenizer, marked } from 'marked';
import { Clipboard } from '@angular/cdk/clipboard';
const copyCodeBlock = '{:copy-code}';
const codeStyleRegex = '^{:code-style="(.*)"}\n';
const autoBlock = '{:auto}';
const targetBlankBlock = '{:target="_blank"}';
// @dynamic
@Injectable({
providedIn: 'root'
})
export class MarkedOptionsService extends MarkedOptions {
renderer = new MarkedRenderer();
headerIds = true;
gfm = true;
breaks = false;
pedantic = false;
smartLists = true;
smartypants = false;
mangle = false;
private renderer2 = new MarkedRenderer();
private id = 1;
constructor(private translate: TranslateService,
private clipboardService: Clipboard,
@Inject(WINDOW) private readonly window: Window,
@Inject(DOCUMENT) private readonly document: Document) {
super();
// @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});
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
const codeContext = processCode(code);
if (codeContext.copyCode) {
const content = postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext);
this.id++;
return this.wrapCopyCode(this.id, content, codeContext);
} else {
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext));
}
};
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('
{
const codeContext = processCode(content);
codeContext.multiline = false;
if (codeContext.copyCode) {
this.id++;
content = this.wrapCopyCode(this.id, codeContext.code, codeContext);
}
return this.renderer2.tablecell(content, flags);
};
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('${content}`;
}
private wrapCopyCode(id: number, content: string, context: CodeContext): string {
let copyCodeButtonClass = 'clipboard-btn';
if (context.multiline) {
copyCodeButtonClass += ' multiline';
}
return `${content}` +
`
${encodeURIComponent(context.code)}` +
`
` +
`
`;
}
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')) {
const text = decodeURIComponent($('#copyCodeId' + id).text());
if (this.clipboardService.copy(text)) {
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();
});
}
}
}
}
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 = '';
return content.replace('', replacement);
}