// 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.$.container.textContent = 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} */ _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} 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; 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('* '); }, _makeLinkedText(content, isPre) { 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. * @param {!Array} blocks * @return {!Array} */ _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'); }, }); })();