a lot of refactoring of drafty to male it usable as a universal media container

This commit is contained in:
or-else
2017-08-24 12:35:14 -07:00
parent 9d1786ab67
commit 0a2abc080f
2 changed files with 219 additions and 232 deletions
+215 -224
View File
@@ -10,119 +10,112 @@
// //
// URLs, @mentions, and #hashtags are extracted and converted into links. // URLs, @mentions, and #hashtags are extracted and converted into links.
// //
// JSON data representation is similar (easily converted) to Draft.js raw formatting. // JSON data representation is inspired by Draft.js raw formatting.
/* /*
Text: Text:
this is *bold*, `code`, _italic_ and ~deleted~. this is *bold*, `code` and _italic_, ~strike~
nested styles: *just bold _bold-italic_* combined *bold and _italic_*
couple of urls: http://www.example.com/path?a=b%20c#fragment, and bolded *www.tinode.co* an url: https://www.example.com/abc#fragment and another _www.tinode.co_
this is a @mention and a #hashtag in a string this is a @mention and a #hashtag in a string
the _#hashtag_ used again second #hashtag
Sample JSON representation of the text above: Sample JSON representation of the text above:
{ {
"blocks":[ "txt":"this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment " +
"and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag",
"fmt":[
{ {
"txt":"this is bold, code, italic and deleted.", "at":8,
"fmt":[ "len":4,
{ "tp":"ST"
"at":8,
"len":4,
"tp":"ST"
},
{
"at":14,
"len":4,
"tp":"CO"
},
{
"at":20,
"len":6,
"tp":"EM"
},
{
"at":31,
"len":7,
"tp":"DL"
}
]
}, },
{ {
"txt":"nested styles: just bold bold-italic", "at":14,
"fmt":[ "len":4,
{ "tp":"CO"
"at":25,
"len":11,
"tp":"EM"
},
{
"at":15,
"len":21,
"tp":"ST"
}
]
}, },
{ {
"txt":"couple of urls: http://www.example.com/path?a=b%20c#fragment, and bolded www.tinode.co", "at":23,
"fmt":[ "len":6,
{ "tp":"EM"
"at":73,
"len":13,
"tp":"ST"
}
],
"ent":[
{
"at":16,
"len":44,
"key":0
},
{
"at":73,
"len":13,
"key":1
}
]
}, },
{ {
"txt":"this is a @mention and a #hashtag in a string", "at":31,
"ent":[ "len":6,
{ "tp":"DL"
"at":10,
"len":8,
"key":2
},
{
"at":25,
"len":8,
"key":3
}
]
}, },
{ {
"txt":"the #hashtag used again", "tp":"BR",
"fmt":[ "len":1,
{ "at":37
"at":4, },
"len":8, {
"tp":"EM" "at":56,
} "len":6,
], "tp":"EM"
"ent":[ },
{ {
"at":4, "at":47,
"len":8, "len":15,
"key":3 "tp":"ST"
} },
] {
"tp":"BR",
"len":1,
"at":62
},
{
"at":120,
"len":13,
"tp":"EM"
},
{
"at":71,
"len":36,
"key":0
},
{
"at":120,
"len":13,
"key":1
},
{
"tp":"BR",
"len":1,
"at":133
},
{
"at":144,
"len":8,
"key":2
},
{
"at":159,
"len":8,
"key":3
},
{
"tp":"BR",
"len":1,
"at":179
},
{
"at":187,
"len":8,
"key":3
},
{
"tp":"BR",
"len":1,
"at":195
} }
], ],
"refs":[ "refs":[
{ {
"tp":"LN", "tp":"LN",
"data":{ "data":{
"url":"http://www.example.com/path?a=b%20c#fragment" "url":"https://www.example.com/abc#fragment"
} }
}, },
{ {
@@ -158,12 +151,12 @@ Sample JSON representation of the text above:
// Emphesized = italic, _italic text_ // Emphesized = italic, _italic text_
{name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/}, {name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/},
// Deleted, ~strike this though~ // Deleted, ~strike this though~
{name: "DL", start: /(?:^|\W)(~)[^\s~]/g, end: /[^\s~](~)(?=$|\W)/}, {name: "DL", start: /(?:^|\W)(~)[^\s~]/, end: /[^\s~](~)(?=$|\W)/},
// Code block `this is monospace` // Code block `this is monospace`
{name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/} {name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/}
]; ];
// RegExps for entity extraction // RegExps for entity extraction (RF = reference)
var ENTITY_TYPES = [ var ENTITY_TYPES = [
// URLs // URLs
{name: "LN", dataName: "url", {name: "LN", dataName: "url",
@@ -182,19 +175,19 @@ Sample JSON representation of the text above:
// Hashtags #hashtag // Hashtags #hashtag
{name: "HT", dataName: "val", {name: "HT", dataName: "val",
pack: function(val) { return {val: val.slice(1)}; }, pack: function(val) { return {val: val.slice(1)}; },
re: /\b#(\w\w+)/g} re: /\B#(\w\w+)/g}
]; ];
// HTML tag name suggestions // HTML tag name suggestions
var TAG_NAMES = { var HTML_TAGS = {
ST: 'b', ST: { name: 'b', isVoid: false },
EM: 'i', EM: { name: 'i', isVoid: false },
DL: 'del', DL: { name: 'del', isVoid: false },
CO: 'tt', CO: { name: 'tt', isVoid: false },
BR: 'br', BR: { name: 'br', isVoid: true },
LN: 'a', LN: { name: 'a', isVoid: false },
MN: 'a', MN: { name: 'a', isVoid: false },
HT: 'a' HT: { name: 'a', isVoid: false }
}; };
// Helpers for converting Drafty to HTML. // Helpers for converting Drafty to HTML.
@@ -203,6 +196,7 @@ Sample JSON representation of the text above:
EM: { open: function() { return '<i>'; }, close: function() { return '</i>'}}, EM: { open: function() { return '<i>'; }, close: function() { return '</i>'}},
DL: { open: function() { return '<del>'; }, close: function() { return '</del>'}}, DL: { open: function() { return '<del>'; }, close: function() { return '</del>'}},
CO: { open: function() { return '<tt>'; }, close: function() { return '</tt>'}}, CO: { open: function() { return '<tt>'; }, close: function() { return '</tt>'}},
BR: { open: function() { return ''; }, close: function() { return '<br/>'}},
LN: { LN: {
open: function(data) { return '<a href="' + data.url + '">'; }, open: function(data) { return '<a href="' + data.url + '">'; },
close: function(data) { return '</a>'; }, close: function(data) { return '</a>'; },
@@ -284,15 +278,16 @@ Sample JSON representation of the text above:
i = si; i = si;
} }
var tag = HTML_TAGS[span.tp] || {};
result.push(formatter.call(context, span.tp, span.data, result.push(formatter.call(context, span.tp, span.data,
forEach(line, start, span.at + span.len, subspans, formatter, context))); tag.isVoid ? null : forEach(line, start, span.at + span.len, subspans, formatter, context)));
start = span.at + span.len; start = span.at + span.len;
} }
// Add the last unformatted range. // Add the last unformatted range.
if (start < end) { if (start < end) {
result.push(formatter.call(context, null, undefined, line.slice(start))); result.push(formatter.call(context, null, undefined, line.slice(start, end)));
} }
return result; return result;
@@ -315,6 +310,7 @@ Sample JSON representation of the text above:
if (start == null) { if (start == null) {
break; break;
} }
// Because javascript RegExp does not support lookbehind, the actual offset may not point // Because javascript RegExp does not support lookbehind, the actual offset may not point
// at the markup character. Find it in the matched string. // at the markup character. Find it in the matched string.
var start_offset = start['index'] + start[0].lastIndexOf(start[1]); var start_offset = start['index'] + start[0].lastIndexOf(start[1]);
@@ -326,7 +322,7 @@ Sample JSON representation of the text above:
index = start_offset + 1; index = start_offset + 1;
// Find the matching closing token. // Find the matching closing token.
var end = re_end.exec(line); var end = re_end ? re_end.exec(line) : null;
if (end == null) { if (end == null) {
break; break;
} }
@@ -449,12 +445,13 @@ Sample JSON representation of the text above:
parse: function(content) { parse: function(content) {
// Split text into lines. It makes further processing easier. // Split text into lines. It makes further processing easier.
var lines = content.split(/\r?\n/); var lines = content.split(/\r?\n/);
// Holds formatted lines
var blocks = [];
// Holds entities referenced from text // Holds entities referenced from text
var entityMap = []; var entityMap = [];
var entityIndex = {}; var entityIndex = {};
//
// Processing lines one by one, hold intermediate result in blx.
var blx = [];
lines.map(function(line) { lines.map(function(line) {
var spans = []; var spans = [];
var entities = []; var entities = [];
@@ -489,8 +486,8 @@ Sample JSON representation of the text above:
// Extract entities from the cleaned up string. // Extract entities from the cleaned up string.
entities = extractEntities(block.txt); entities = extractEntities(block.txt);
var ranges = [];
if (entities.length > 0) { if (entities.length > 0) {
var ranges = [];
for (var i in entities) { for (var i in entities) {
// {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name} // {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name}
var ent = entities[i]; var ent = entities[i];
@@ -505,12 +502,42 @@ Sample JSON representation of the text above:
block.ent = ranges; block.ent = ranges;
} }
blocks.push(block); blx.push(block);
}); });
var result = {blocks: blocks}; var result = {txt: ""};
if (entityMap.length > 0) {
result.refs = entityMap; // Merge lines and save line breaks as BR inline formatting.
if (blx.length > 0) {
result.txt = blx[0].txt;
result.fmt = (blx[0].fmt || []).concat(blx[0].ent || []);
for (var i = 1; i<blx.length; i++) {
var block = blx[i];
var offset = result.txt.length + 1;
result.fmt.push({tp: "BR", len: 1, at: offset - 1});
result.txt += " " + block.txt;
if (block.fmt) {
result.fmt = result.fmt.concat(block.fmt.map(function(s) {
s.at += offset; return s;
}));
}
if (block.ent) {
result.fmt = result.fmt.concat(block.ent.map(function(s) {
s.at += offset; return s;
}));
}
}
if (result.fmt.length == 0) {
delete result.fmt;
}
if (entityMap.length > 0) {
result.refs = entityMap;
}
} }
return result; return result;
}, },
@@ -526,46 +553,39 @@ Sample JSON representation of the text above:
* @return HTML-representation of content * @return HTML-representation of content
*/ */
unsafeToHTML: function(content) { unsafeToHTML: function(content) {
var {blocks} = content; var {txt, fmt, refs} = content;
var {refs} = content;
if (!blocks) { var markup = [];
return ''; if (fmt) {
} for (var i in fmt) {
var range = fmt[i];
var text = []; var tp = range.tp, data;
for (var i in blocks) { if (!tp) {
var line = blocks[i].txt;
var markup = [];
if (blocks[i].fmt) {
for (var j in blocks[i].fmt) {
var range = blocks[i].fmt[j];
markup.push({idx: range.at, what: DECORATORS[range.tp].open()});
markup.push({idx: range.at + range.len, what: DECORATORS[range.tp].close()});
}
}
if (blocks[i].ent) {
for (var j in blocks[i].ent) {
var range = blocks[i].ent[j];
var entity = refs[range.key]; var entity = refs[range.key];
markup.push({idx: range.at, what: DECORATORS[entity.tp].open(entity.data)}); if (entity) {
markup.push({idx: range.at + range.len, what: DECORATORS[entity.tp].close(entity.data)}); tp = entity.tp;
data = entity.data;
}
}
if (DECORATORS[tp]) {
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
} }
} }
if (markup.length > 0) {
markup.sort(function(a, b) {
return b.idx - a.idx; // in descending order
});
for (var j in markup) {
line = splice(line, markup[j].idx, markup[j].what);
}
}
text.push(line);
} }
return text.join("<br/>");
markup.sort(function(a, b) {
return b.idx - a.idx; // in descending order
});
for (var i in markup) {
if (markup[i].what) {
txt = splice(txt, markup[i].idx, markup[i].what);
}
}
return txt;
}, },
/** /**
@@ -578,116 +598,87 @@ Sample JSON representation of the text above:
* @return {Object} context * @return {Object} context
*/ */
format: function(content, formatter, context) { format: function(content, formatter, context) {
var {blocks} = content; var {txt, fmt, refs} = content;
var {refs} = content;
if (!blocks) { if (!fmt) {
return []; return [txt];
} }
var result = []; var spans = [].concat(fmt);
// Process blocks one by one // Soft spans first by start index (asc) then by length (desc).
for (var i = 0; i < blocks.length; i++) { spans.sort(function(a, b) {
var block = blocks[i]; if (a.at - b.at == 0) {
var spans = []; return b.len - a.len; // longer one comes first (<0)
// Merge markup ranges and sort in ascending order.
if (block.fmt) {
spans = spans.concat(block.fmt);
} }
if (block.ent) { return a.at - b.at;
spans = spans.concat(block.ent); });
// Denormalize entities into spans. Create a copy of the objects to leave
// original Drafty object unchanged.
spans = spans.map(function(s) {
var data;
var tp = s.tp;
if (s.key + 1 > 0) {
data = refs[s.key].data;
tp = refs[s.key].tp;
} }
return {tp: tp, data: data, at: s.at, len: s.len};
});
// Soft spans first by start index (asc) then by length (desc). return forEach(txt, 0, txt.length, spans, formatter, context);
spans.sort(function(a, b) {
if (a.at - b.at == 0) {
return b.len - a.len; // longer one comes first (<0)
}
return a.at - b.at;
});
// Denormalize entities.
spans.map(function(s) {
if (s.key + 1 > 0) {
s.data = refs[s.key].data;
s.tp = refs[s.key].tp;
}
});
result = result.concat(forEach(block.txt, 0, block.txt.length - 1, spans, formatter, context));
if (i != blocks.length - 1) {
// Add line break
result.push(formatter.call(context, "BR"));
}
}
return result;
}, },
/** /**
* Given structured representation of rich text, convert it to plain text. * Given structured representation of rich text, convert it to plain text.
* The only structure preserved is line breaks as \n.
* *
* @param {drafty} content - content to convert to plain text. * @param {drafty} content - content to convert to plain text.
*/ */
toPlainText: function(content) { toPlainText: function(content) {
var {blocks} = content; return content.text;
if (!blocks) {
return '';
}
var text = blocks[0].txt;
for (var i = 1; i < blocks.length; i++) {
text += (lineBreak ? lineBreak.call(context) : "\n") + blocks[i].txt;
}
return text;
}, },
/** /**
* Returns true if content has no markup and no entities. * Returns true if content has no markup and no entities.
* *
* @param {drafty} content - content to check for presence of markup. * @param {drafty} content - content to check for presence of markup.
* @param {boolean} noLineBreaks - true to treat line breaks as markup, false
* to treat them as plain text.
* @returns true is content is plain text, false otherwise. * @returns true is content is plain text, false otherwise.
*/ */
isPlainText: function(content, noLineBreaks) { isPlainText: function(content, noLineBreaks) {
if (content.refs) { return !(content.fmt || content.refs);
return false;
}
var {blocks} = content;
if (!blocks) {
return true;
}
if (noLineBreaks && blocks.length > 1) {
return false;
}
for (var i in blocks) {
if (blocks[i].fmt) {
return false;
}
}
return true;
}, },
/**
* Get HTML tag for a given two-letter style name
* @param {string} style - two-letter style, like ST or LN
* @returns
*/
tagName: function(style) { tagName: function(style) {
return TAG_NAMES[style]; return HTML_TAGS[style] ? HTML_TAGS[style].name : undefined;
}, },
/**
* For a given data bundle generate an object with HTML attributes,
* for instance, given {url: "http://www.example.com/"} return
* {href: "http://www.example.com/"}
* @param {stting} style - tw-letter style to generate attributes for.
* @param {Object} data - data bundle to convert to attributes
* @returns object with HTML attributes.
*/
attrValue: function(style, data) { attrValue: function(style, data) {
if (data && DECORATORS[style]) { if (data && DECORATORS[style]) {
return DECORATORS[style].props(data); return DECORATORS[style].props(data);
} }
return undefined; return undefined;
},
/**
* Content MIME type.
* @returns string suitabe for HTTP Content-Type field.
*/
getContentType: function() {
return "text/x-drafty";
} }
}; };
+4 -8
View File
@@ -2030,6 +2030,8 @@
var elementKey = 0; var elementKey = 0;
var formatter = function(style, data, values) { var formatter = function(style, data, values) {
// console.log("El: " + style + ", data: " + JSON.stringify(data) + ", values: " + JSON.stringify(values));
elementKey += 1; elementKey += 1;
var el = Drafty.tagName(style); var el = Drafty.tagName(style);
if (el) { if (el) {
@@ -2049,14 +2051,8 @@
(this.props.sequence === "single" || this.props.sequence === "last")); (this.props.sequence === "single" || this.props.sequence === "last"));
var content = this.props.content; var content = this.props.content;
if (this.props.mimeType === "text/x-drafty+json") { if (this.props.mimeType === Drafty.getContentType()) {
content = React.createElement('span', null, Drafty.format(content, formatter)); content = React.createElement('span', null, Drafty.format(content, formatter));
} else {
// Handle line breaks in plain text.
// Regex capure group keep line breaks in the array.
content = content.split(/(\n)/g).map(function(line, idx) {
return line.match("\n") ? <br key={idx} /> : line;
});
} }
return ( return (
@@ -2655,7 +2651,7 @@
topic.publish(msg); topic.publish(msg);
} else { } else {
// Formatted message. Send it as a Drafty object. // Formatted message. Send it as a Drafty object.
topic.publish(dft, undefined, "text/x-drafty+json"); topic.publish(dft, undefined, Drafty.getContentType());
} }
}, },
// User chose a Sign Up menu item. // User chose a Sign Up menu item.