316 lines
8.6 KiB
TypeScript
316 lines
8.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* 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 '../gr-linked-text/gr-linked-text';
|
|
import '../../../styles/shared-styles';
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
|
import {customElement, property} from '@polymer/decorators';
|
|
import {htmlTemplate} from './gr-formatted-text_html';
|
|
import {CommentLinks} from '../../../types/common';
|
|
|
|
const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
|
|
|
|
interface Block {
|
|
type: string;
|
|
text?: string;
|
|
blocks?: Block[];
|
|
items?: string[];
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'gr-formatted-text': GrFormattedText;
|
|
}
|
|
}
|
|
|
|
export interface GrFormattedText {
|
|
$: {
|
|
container: HTMLElement;
|
|
};
|
|
}
|
|
|
|
@customElement('gr-formatted-text')
|
|
export class GrFormattedText extends GestureEventListeners(
|
|
LegacyElementMixin(PolymerElement)
|
|
) {
|
|
static get template() {
|
|
return htmlTemplate;
|
|
}
|
|
|
|
@property({type: String, observer: '_contentChanged'})
|
|
content?: string;
|
|
|
|
@property({type: Object})
|
|
config?: CommentLinks;
|
|
|
|
@property({type: Boolean})
|
|
noTrailingMargin = false;
|
|
|
|
static get observers() {
|
|
return ['_contentOrConfigChanged(content, config)'];
|
|
}
|
|
|
|
/** @override */
|
|
ready() {
|
|
super.ready();
|
|
if (this.noTrailingMargin) {
|
|
this.classList.add('noTrailingMargin');
|
|
}
|
|
}
|
|
|
|
_contentChanged(content: string) {
|
|
// In the case where the config may not be set (perhaps due to the
|
|
// request for it still being in flight), set the content anyway to
|
|
// prevent waiting on the config to display the text.
|
|
if (this.config) return;
|
|
this._contentOrConfigChanged(content);
|
|
}
|
|
|
|
/**
|
|
* Given a source string, update the DOM inside #container.
|
|
*/
|
|
_contentOrConfigChanged(content?: string) {
|
|
const container = this.$.container;
|
|
|
|
// Remove existing content.
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
// Add new content.
|
|
for (const node of this._computeNodes(this._computeBlocks(content))) {
|
|
if (node) container.appendChild(node);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a source string, parse into an array of block objects. Each block
|
|
* has a `type` property which takes any of the following values.
|
|
* * 'paragraph'
|
|
* * 'quote' (Block quote.)
|
|
* * 'pre' (Pre-formatted text.)
|
|
* * 'list' (Unordered list.)
|
|
* * 'code' (code blocks.)
|
|
*
|
|
* For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
|
|
* property that maps to a string of the block's content.
|
|
*
|
|
* For blocks of type 'list', there is an `items` property that maps to a
|
|
* list of strings representing the list items.
|
|
*
|
|
* For blocks of type 'quote', there is a `blocks` property that maps to a
|
|
* list of blocks contained in the quote.
|
|
*
|
|
* NOTE: Strings appearing in all block objects are NOT escaped.
|
|
*/
|
|
_computeBlocks(content?: string): Block[] {
|
|
if (!content) return [];
|
|
|
|
const result = [];
|
|
const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (!lines[i].length) {
|
|
continue;
|
|
}
|
|
|
|
if (this._isCodeMarkLine(lines[i])) {
|
|
// handle multi-line code
|
|
let nextI = i + 1;
|
|
while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
|
|
nextI++;
|
|
}
|
|
|
|
if (this._isCodeMarkLine(lines[nextI])) {
|
|
result.push({
|
|
type: 'code',
|
|
text: lines.slice(i + 1, nextI).join('\n'),
|
|
});
|
|
i = nextI;
|
|
continue;
|
|
}
|
|
|
|
// otherwise treat it as regular line and continue
|
|
// check for other cases
|
|
}
|
|
|
|
if (this._isSingleLineCode(lines[i])) {
|
|
// no guard check as _isSingleLineCode tested on the pattern
|
|
const codeContent = lines[i].match(CODE_MARKER_PATTERN)![2];
|
|
result.push({type: 'code', text: codeContent});
|
|
} else if (this._isList(lines[i])) {
|
|
let nextI = i + 1;
|
|
while (this._isList(lines[nextI])) {
|
|
nextI++;
|
|
}
|
|
result.push(this._makeList(lines.slice(i, nextI)));
|
|
i = nextI - 1;
|
|
} else if (this._isQuote(lines[i])) {
|
|
let nextI = i + 1;
|
|
while (this._isQuote(lines[nextI])) {
|
|
nextI++;
|
|
}
|
|
const blockLines = lines
|
|
.slice(i, nextI)
|
|
.map(l => l.replace(/^[ ]?>[ ]?/, ''));
|
|
result.push({
|
|
type: 'quote',
|
|
blocks: this._computeBlocks(blockLines.join('\n')),
|
|
});
|
|
i = nextI - 1;
|
|
} else if (this._isPreFormat(lines[i])) {
|
|
let nextI = i + 1;
|
|
// include pre or all regular lines but stop at next new line
|
|
while (
|
|
this._isPreFormat(lines[nextI]) ||
|
|
(this._isRegularLine(lines[nextI]) && lines[nextI].length)
|
|
) {
|
|
nextI++;
|
|
}
|
|
result.push({
|
|
type: 'pre',
|
|
text: lines.slice(i, nextI).join('\n'),
|
|
});
|
|
i = nextI - 1;
|
|
} else {
|
|
let nextI = i + 1;
|
|
while (this._isRegularLine(lines[nextI])) {
|
|
nextI++;
|
|
}
|
|
result.push({
|
|
type: 'paragraph',
|
|
text: lines.slice(i, nextI).join('\n'),
|
|
});
|
|
i = nextI - 1;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Take a block of comment text that contains a list, generate appropriate
|
|
* block objects and append them to the output list.
|
|
*
|
|
* * Item one.
|
|
* * Item two.
|
|
* * item three.
|
|
*
|
|
* TODO(taoalpha): maybe we should also support nested list
|
|
*
|
|
* @param lines The block containing the list.
|
|
*/
|
|
_makeList(lines: string[]) {
|
|
const items = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
let line = lines[i];
|
|
line = line.substring(1).trim();
|
|
items.push(line);
|
|
}
|
|
return {type: 'list', items};
|
|
}
|
|
|
|
_isRegularLine(line: string) {
|
|
// line can not be recognized by existing patterns
|
|
if (line === undefined) return false;
|
|
return (
|
|
!this._isQuote(line) &&
|
|
!this._isCodeMarkLine(line) &&
|
|
!this._isSingleLineCode(line) &&
|
|
!this._isList(line) &&
|
|
!this._isPreFormat(line)
|
|
);
|
|
}
|
|
|
|
_isQuote(line: string) {
|
|
return line && (line.startsWith('> ') || line.startsWith(' > '));
|
|
}
|
|
|
|
_isCodeMarkLine(line: string) {
|
|
return line && line.trim() === '```';
|
|
}
|
|
|
|
_isSingleLineCode(line: string) {
|
|
return line && CODE_MARKER_PATTERN.test(line);
|
|
}
|
|
|
|
_isPreFormat(line: string) {
|
|
return line && /^[ \t]/.test(line);
|
|
}
|
|
|
|
_isList(line: string) {
|
|
return line && /^[-*] /.test(line);
|
|
}
|
|
|
|
_makeLinkedText(content = '', isPre?: boolean) {
|
|
const text = document.createElement('gr-linked-text');
|
|
text.config = this.config;
|
|
text.content = content;
|
|
text.pre = true;
|
|
if (isPre) {
|
|
text.classList.add('pre');
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Map an array of block objects to an array of DOM nodes.
|
|
*/
|
|
_computeNodes(blocks: Block[]): HTMLElement[] {
|
|
return blocks.map(block => {
|
|
if (block.type === 'paragraph') {
|
|
const p = document.createElement('p');
|
|
p.appendChild(this._makeLinkedText(block.text));
|
|
return p;
|
|
}
|
|
|
|
if (block.type === 'quote') {
|
|
const bq = document.createElement('blockquote');
|
|
for (const node of this._computeNodes(block.blocks || [])) {
|
|
if (node) bq.appendChild(node);
|
|
}
|
|
return bq;
|
|
}
|
|
|
|
if (block.type === 'code') {
|
|
const code = document.createElement('code');
|
|
code.textContent = block.text || '';
|
|
return code;
|
|
}
|
|
|
|
if (block.type === 'pre') {
|
|
return this._makeLinkedText(block.text, true);
|
|
}
|
|
|
|
if (block.type === 'list') {
|
|
const ul = document.createElement('ul');
|
|
const items = block.items || [];
|
|
for (const item of items) {
|
|
const li = document.createElement('li');
|
|
li.appendChild(this._makeLinkedText(item));
|
|
ul.appendChild(li);
|
|
}
|
|
return ul;
|
|
}
|
|
|
|
console.warn('Unrecognized type.');
|
|
return document.createElement('span');
|
|
});
|
|
}
|
|
}
|