diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 00ebd4957b..af666b05d2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -354,6 +354,7 @@ limitations under the License.
permitted-labels="[[_change.permitted_labels]]"
diff-drafts="[[_diffDrafts]]"
server-config="[[serverConfig]]"
+ project-config="[[_projectConfig]]"
on-send="_handleReplySent"
on-cancel="_handleReplyCancel"
on-autogrow="_handleReplyAutogrow"
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 810200673b..40dffcc7a0 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -16,14 +16,13 @@ limitations under the License.
-
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 8e13684691..a7be3b7e15 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -18,7 +18,7 @@ limitations under the License.
-
+
@@ -26,8 +26,9 @@ limitations under the License.
+
+
+
+
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
new file mode 100644
index 0000000000..3d2d3d3ccd
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -0,0 +1,235 @@
+// 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';
+
+ var QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
+
+ Polymer({
+ is: 'gr-formatted-text',
+
+ properties: {
+ content: String,
+ config: Object,
+ },
+
+ observers: [
+ '_contentOrConfigChanged(content, config)',
+ ],
+
+ /**
+ * Given a source string, update the DOM inside #container.
+ */
+ _contentOrConfigChanged: function(content) {
+ var container = Polymer.dom(this.$.container);
+
+ // Remove existing content.
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ // Add new content.
+ this._computeNodes(this._computeBlocks(content)).forEach(function(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 follwoing values.
+ * * 'paragraph'
+ * * 'quote' (Block quote.)
+ * * 'pre' (Pre-formatted text.)
+ * * 'list' (Unordered list.)
+ *
+ * For blocks of type 'paragraph', 'quote' or '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.
+ *
+ * NOTE: Strings appearing in all block objects are NOT escaped.
+ *
+ * @param {string} content
+ * @return {!Array}
+ */
+ _computeBlocks: function(content) {
+ if (!content) { return []; }
+
+ var result = [];
+ var split = content.split('\n\n');
+ var p;
+
+ for (var 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: function(p, out) {
+ var block = null;
+ var inList = false;
+ var inParagraph = false;
+ var lines = p.split('\n');
+ var line;
+
+ for (var 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: function(p) {
+ var quotedLines = p
+ .split('\n')
+ .map(function(l) { return l.replace(/^[ ]?>[ ]?/, ''); })
+ .join('\n');
+ return {
+ type: 'quote',
+ blocks: this._computeBlocks(quotedLines),
+ };
+ },
+
+ _isQuote: function(p) {
+ return p.indexOf('> ') === 0 || p.indexOf(' > ') === 0;
+ },
+
+ _isPreFormat: function(p) {
+ return p.indexOf('\n ') !== -1 || p.indexOf('\n\t') !== -1 ||
+ p.indexOf(' ') === 0 || p.indexOf('\t') === 0;
+ },
+
+ _isList: function(p) {
+ return p.indexOf('\n- ') !== -1 || p.indexOf('\n* ') !== -1 ||
+ p.indexOf('- ') === 0 || p.indexOf('* ') === 0;
+ },
+
+ _makeLinkedText: function(content, isPre) {
+ var text = document.createElement('gr-linked-text');
+ text.config = this.config;
+ text.content = content;
+ if (isPre) {
+ text.pre = true;
+ }
+ return text;
+ },
+
+ /**
+ * Map an array of block objects to an array of DOM nodes.
+ * @param {!Array} blocks
+ * @return {!Array}
+ */
+ _computeNodes: function(blocks) {
+ return blocks.map(function(block) {
+ if (block.type === 'paragraph') {
+ var p = document.createElement('p');
+ p.appendChild(this._makeLinkedText(block.text));
+ return p;
+ }
+
+ if (block.type === 'quote') {
+ var bq = document.createElement('blockquote');
+ this._computeNodes(block.blocks).forEach(function(node) {
+ bq.appendChild(node);
+ });
+ return bq;
+ }
+
+ if (block.type === 'pre') {
+ return this._makeLinkedText(block.text, true);
+ }
+
+ if (block.type === 'list') {
+ var ul = document.createElement('ul');
+ block.items.forEach(function(item) {
+ var li = document.createElement('li');
+ li.appendChild(this._makeLinkedText(item));
+ ul.appendChild(li);
+ }.bind(this));
+ return ul;
+ }
+ }.bind(this));
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
new file mode 100644
index 0000000000..85610588cd
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -0,0 +1,350 @@
+
+
+
+
+