diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx
index 837da4359..e5b9d5d77 100644
--- a/client/homebrew/pages/userPage/userPage.jsx
+++ b/client/homebrew/pages/userPage/userPage.jsx
@@ -42,7 +42,7 @@ const UserPage = createClass({
renderBrews : function(brews){
if(!brews || !brews.length) return
No Brews.
;
- const sortedBrews = this.sortBrews(brews, this.state.sortType);
+ const sortedBrews = this.sortBrews(brews);
return _.map(sortedBrews, (brew, idx)=>{
return ;
@@ -50,6 +50,7 @@ const UserPage = createClass({
},
sortBrewOrder : function(brew){
+ if(!brew.title){brew.title = 'No Title';};
const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : brew.createdAt,
diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js
index 9ae71214a..3a341f379 100644
--- a/shared/naturalcrit/markdown.js
+++ b/shared/naturalcrit/markdown.js
@@ -234,7 +234,188 @@ const definitionLists = {
}
};
-Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
+const spanTable = {
+ name : 'spanTable',
+ level : 'block', // Is this a block-level or inline-level tokenizer?
+ start(src) { return src.match(/^\n *([^\n ].*\|.*)\n/)?.index; }, // Hint to Marked.js to stop and check for a match
+ tokenizer(src, tokens) {
+ //const regex = this.tokenizer.rules.block.table;
+ const regex = new RegExp('^ *([^\\n ].*\\|.*\\n(?:.*?[^\\s].*\\n)*?)' // Header
+ + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)\\|?' // Align
+ + '(?:\\n *((?:(?!\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells
+ + '(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}'
+ + '(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |'
+ + '<\\/?(?:address|article|aside|base|basefont|blockquote|body|'
+ + 'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)'); // Cells
+ const cap = regex.exec(src);
+
+ if(cap) {
+ const item = {
+ type : 'spanTable',
+ header : cap[1].replace(/\n$/, '').split('\n'),
+ align : cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ rows : cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
+ };
+
+ // Get first header row to determine how many columns
+ item.header[0] = splitCells(item.header[0]);
+
+ const colCount = item.header[0].reduce((length, header)=>{
+ return length + header.colspan;
+ }, 0);
+
+ if(colCount === item.align.length) {
+ item.raw = cap[0];
+
+ let i, j, k, row;
+
+ // Get alignment row (:---:)
+ let l = item.align.length;
+
+ for (i = 0; i < l; i++) {
+ if(/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if(/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if(/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ // Get any remaining header rows
+ l = item.header.length;
+ for (i = 1; i < l; i++) {
+ item.header[i] = splitCells(item.header[i], colCount);
+ }
+
+ // Get main table cells
+ l = item.rows.length;
+ for (i = 0; i < l; i++) {
+ item.rows[i] = splitCells(item.rows[i], colCount);
+ }
+
+ // header child tokens
+ l = item.header.length;
+ for (j = 0; j < l; j++) {
+ row = item.header[j];
+ for (k = 0; k < row.length; k++) {
+ row[k].tokens = [];
+ this.inlineTokens(row[k].text, row[k].tokens);
+ }
+ }
+
+ // cell child tokens
+ l = item.rows.length;
+ for (j = 0; j < l; j++) {
+ row = item.rows[j];
+ for (k = 0; k < row.length; k++) {
+ row[k].tokens = [];
+ this.inlineTokens(row[k].text, row[k].tokens);
+ }
+ }
+ return item;
+ }
+ }
+ },
+ renderer(token) {
+ let i, j, row, cell, col, text;
+ let output = ``;
+ output += ``;
+ for (i = 0; i < token.header.length; i++) {
+ row = token.header[i];
+ let col = 0;
+ output += ``;
+ for (j = 0; j < row.length; j++) {
+ cell = row[j];
+ text = this.parseInline(cell.tokens);
+ output += getTableCell(text, cell.colspan, 'th', token.align[col]);
+ col += cell.colspan;
+ }
+ output += `
`;
+ }
+ output += ``;
+ if(token.rows.length) {
+ output += ``;
+ for (i = 0; i < token.rows.length; i++) {
+ row = token.rows[i];
+ col = 0;
+ output += ``;
+ for (j = 0; j < row.length; j++) {
+ cell = row[j];
+ text = this.parseInline(cell.tokens);
+ output += getTableCell(text, cell.colspan, 'td', token.align[col]);
+ col += cell.colspan;
+ }
+ output += `
`;
+ }
+ output += ``;
+ }
+ output += `
`;
+ return output;
+ }
+};
+
+const getTableCell = (text, colspan, type, align)=>{
+ const tag = `<${type}`
+ + `${colspan > 1 ? ` colspan=${colspan}` : ''}`
+ + `${align ? ` align=${align}` : ''}>`;
+ return `${tag + text}${type}>\n`;
+};
+
+const splitCells = (tableRow, count)=>{
+ // ensure that every cell-delimiting pipe has a space
+ // before it to distinguish it from an escaped pipe
+ const row = tableRow.replace(/(\|+)/g, (match, p1, offset, str)=>{
+ let escaped = false,
+ curr = offset;
+ while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
+ if(escaped) {
+ // odd number of slashes means | is escaped
+ // so we leave it and the slashes alone
+ return p1;
+ } else {
+ // add space before unescaped |
+ return ` ${p1}`;
+ }
+ });
+
+ const cells = row.split(/(?: \||(?<=\|)\|)(?=[^\|]|$)/g);
+ let i = 0;
+
+ // First/last cell in a row cannot be empty if it has no leading/trailing pipe
+ if(!cells[0].trim()) { cells.shift(); }
+ if(!cells[cells.length - 1].trim()) { cells.pop(); }
+
+ let numCols = 0;
+
+ for (; i < cells.length; i++) {
+ const trimmedCell = cells[i].split(/(? count) {
+ cells.splice(count);
+ } else {
+ while (numCols < count) {
+ cells.push({
+ colspan : 1,
+ text : ''
+ });
+ numCols += 1;
+ }
+ }
+ return cells;
+};
+
+Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, spanTable] });
Markdown.use(mustacheInjectBlock);
Markdown.use({ smartypants: true });