|  | // 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'); | 
|  | }, | 
|  | }); | 
|  | })(); |