289 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // 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<!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;
 | |
|               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<!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');
 | |
|     },
 | |
|   });
 | |
| })();
 | 
