blob: feae17314ee285a5348572eb6cbba05273046009 [file] [log] [blame]
/**
* @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.
*/
(function() {
'use strict';
// eslint-disable-next-line no-unused-vars
const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
Polymer({
is: 'gr-formatted-text',
properties: {
content: {
type: String,
observer: '_contentChanged',
},
config: Object,
noTrailingMargin: {
type: Boolean,
value: false,
},
},
observers: [
'_contentOrConfigChanged(content, config)',
],
ready() {
if (this.noTrailingMargin) {
this.classList.add('noTrailingMargin');
}
},
/**
* Get the plain text as it appears in the generated DOM.
*
* This differs from the `content` property in that it will not include
* formatting markers such as > characters to make quotes or * and - markers
* to make list items.
*
* @return {string}
*/
getTextContent() {
return this._blocksToText(this._computeBlocks(this.content));
},
_contentChanged(content) {
// 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) {
const container = Polymer.dom(this.$.container);
// Remove existing content.
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Add new content.
for (const node of this._computeNodes(this._computeBlocks(content))) {
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 follwoing values.
* * 'paragraph'
* * 'quote' (Block quote.)
* * 'pre' (Pre-formatted text.)
* * 'list' (Unordered list.)
*
* For blocks of type 'paragraph' and 'pre' 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.
*
* @param {string} content
* @return {!Array<!Object>}
*/
_computeBlocks(content) {
if (!content) { return []; }
const result = [];
const split = content.split('\n\n');
let p;
for (let i = 0; i < split.length; i++) {
p = split[i];
if (!p.length) { continue; }
if (this._isQuote(p)) {
result.push(this._makeQuote(p));
} else if (this._isPreFormat(p)) {
result.push({type: 'pre', text: p});
} else if (this._isList(p)) {
this._makeList(p, result);
} else {
result.push({type: 'paragraph', text: p});
}
}
return result;
},
/**
* Take a block of comment text that contains a list and potentially
* a paragraph (but does not contain blank lines), generate appropriate
* block objects and append them to the output list.
*
* In simple cases, this will generate a single list block. For example, on
* the following input.
*
* * Item one.
* * Item two.
* * item three.
*
* However, if the list starts with a paragraph, it will need to also
* generate that paragraph. Consider the following input.
*
* A bit of text describing the context of the list:
* * List item one.
* * List item two.
* * Et cetera.
*
* In this case, `_makeList` generates a paragraph block object
* containing the non-bullet-prefixed text, followed by a list block.
*
* @param {!string} p The block containing the list (as well as a
* potential paragraph).
* @param {!Array<!Object>} out The list of blocks to append to.
*/
_makeList(p, out) {
let block = null;
let inList = false;
let inParagraph = false;
const lines = p.split('\n');
let line;
for (let i = 0; i < lines.length; i++) {
line = lines[i];
if (line[0] === '-' || line[0] === '*') {
// The next line looks like a list item. If not building a list
// already, then create one. Remove the list item marker (* or -) from
// the line.
if (!inList) {
if (inParagraph) {
// Add the finished paragraph block to the result.
inParagraph = false;
if (block !== null) {
out.push(block);
}
}
inList = true;
block = {type: 'list', items: []};
}
line = line.substring(1).trim();
} else if (!inList) {
// Otherwise, if a list has not yet been started, but the next line
// does not look like a list item, then add the line to a paragraph
// block. If a paragraph block has not yet been started, then create
// one.
if (!inParagraph) {
inParagraph = true;
block = {type: 'paragraph', text: ''};
} else {
block.text += ' ';
}
block.text += line;
continue;
}
block.items.push(line);
}
if (block !== null) {
out.push(block);
}
},
_makeQuote(p) {
const quotedLines = p
.split('\n')
.map(l => l.replace(/^[ ]?>[ ]?/, ''))
.join('\n');
return {
type: 'quote',
blocks: this._computeBlocks(quotedLines),
};
},
_isQuote(p) {
return p.startsWith('> ') || p.startsWith(' > ');
},
_isPreFormat(p) {
return p.includes('\n ') || p.includes('\n\t') ||
p.startsWith(' ') || p.startsWith('\t');
},
_isList(p) {
return p.includes('\n- ') || p.includes('\n* ') ||
p.startsWith('- ') || p.startsWith('* ');
},
/**
* @param {string} content
* @param {boolean=} opt_isPre
*/
_makeLinkedText(content, opt_isPre) {
const text = document.createElement('gr-linked-text');
text.config = this.config;
text.content = content;
text.pre = true;
if (opt_isPre) {
text.classList.add('pre');
}
return text;
},
/**
* Map an array of block objects to an array of DOM nodes.
*
* @param {!Array<!Object>} blocks
* @return {!Array<!HTMLElement>}
*/
_computeNodes(blocks) {
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)) {
bq.appendChild(node);
}
return bq;
}
if (block.type === 'pre') {
return this._makeLinkedText(block.text, true);
}
if (block.type === 'list') {
const ul = document.createElement('ul');
for (const item of block.items) {
const li = document.createElement('li');
li.appendChild(this._makeLinkedText(item));
ul.appendChild(li);
}
return ul;
}
});
},
_blocksToText(blocks) {
return blocks.map(block => {
if (block.type === 'paragraph' || block.type === 'pre') {
return block.text;
}
if (block.type === 'quote') {
return this._blocksToText(block.blocks);
}
if (block.type === 'list') {
return block.items.join('\n');
}
}).join('\n\n');
},
});
})();