From 0a2abc080fff022c80ecaea14f2cd8066647d86d Mon Sep 17 00:00:00 2001 From: or-else Date: Thu, 24 Aug 2017 12:35:14 -0700 Subject: [PATCH] a lot of refactoring of drafty to male it usable as a universal media container --- drafty.js | 439 ++++++++++++++++++++++++++--------------------------- index.html | 12 +- 2 files changed, 219 insertions(+), 232 deletions(-) diff --git a/drafty.js b/drafty.js index dd1e366a..f8cbbe0e 100644 --- a/drafty.js +++ b/drafty.js @@ -10,119 +10,112 @@ // // 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: - this is *bold*, `code`, _italic_ and ~deleted~. - nested styles: *just bold _bold-italic_* - couple of urls: http://www.example.com/path?a=b%20c#fragment, and bolded *www.tinode.co* + 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 - the _#hashtag_ used again + second #hashtag 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.", - "fmt":[ - { - "at":8, - "len":4, - "tp":"ST" - }, - { - "at":14, - "len":4, - "tp":"CO" - }, - { - "at":20, - "len":6, - "tp":"EM" - }, - { - "at":31, - "len":7, - "tp":"DL" - } - ] + "at":8, + "len":4, + "tp":"ST" }, { - "txt":"nested styles: just bold bold-italic", - "fmt":[ - { - "at":25, - "len":11, - "tp":"EM" - }, - { - "at":15, - "len":21, - "tp":"ST" - } - ] + "at":14, + "len":4, + "tp":"CO" }, { - "txt":"couple of urls: http://www.example.com/path?a=b%20c#fragment, and bolded www.tinode.co", - "fmt":[ - { - "at":73, - "len":13, - "tp":"ST" - } - ], - "ent":[ - { - "at":16, - "len":44, - "key":0 - }, - { - "at":73, - "len":13, - "key":1 - } - ] + "at":23, + "len":6, + "tp":"EM" }, { - "txt":"this is a @mention and a #hashtag in a string", - "ent":[ - { - "at":10, - "len":8, - "key":2 - }, - { - "at":25, - "len":8, - "key":3 - } - ] + "at":31, + "len":6, + "tp":"DL" }, { - "txt":"the #hashtag used again", - "fmt":[ - { - "at":4, - "len":8, - "tp":"EM" - } - ], - "ent":[ - { - "at":4, - "len":8, - "key":3 - } - ] + "tp":"BR", + "len":1, + "at":37 + }, + { + "at":56, + "len":6, + "tp":"EM" + }, + { + "at":47, + "len":15, + "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":[ { "tp":"LN", "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_ {name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/}, // 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` {name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/} ]; - // RegExps for entity extraction + // RegExps for entity extraction (RF = reference) var ENTITY_TYPES = [ // URLs {name: "LN", dataName: "url", @@ -182,19 +175,19 @@ Sample JSON representation of the text above: // Hashtags #hashtag {name: "HT", dataName: "val", pack: function(val) { return {val: val.slice(1)}; }, - re: /\b#(\w\w+)/g} + re: /\B#(\w\w+)/g} ]; // HTML tag name suggestions - var TAG_NAMES = { - ST: 'b', - EM: 'i', - DL: 'del', - CO: 'tt', - BR: 'br', - LN: 'a', - MN: 'a', - HT: 'a' + var HTML_TAGS = { + ST: { name: 'b', isVoid: false }, + EM: { name: 'i', isVoid: false }, + DL: { name: 'del', isVoid: false }, + CO: { name: 'tt', isVoid: false }, + BR: { name: 'br', isVoid: true }, + LN: { name: 'a', isVoid: false }, + MN: { name: 'a', isVoid: false }, + HT: { name: 'a', isVoid: false } }; // Helpers for converting Drafty to HTML. @@ -203,6 +196,7 @@ Sample JSON representation of the text above: EM: { open: function() { return ''; }, close: function() { return ''}}, DL: { open: function() { return ''; }, close: function() { return ''}}, CO: { open: function() { return ''; }, close: function() { return ''}}, + BR: { open: function() { return ''; }, close: function() { return '
'}}, LN: { open: function(data) { return ''; }, close: function(data) { return ''; }, @@ -284,15 +278,16 @@ Sample JSON representation of the text above: i = si; } + var tag = HTML_TAGS[span.tp] || {}; 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; } // Add the last unformatted range. 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; @@ -315,6 +310,7 @@ Sample JSON representation of the text above: if (start == null) { break; } + // Because javascript RegExp does not support lookbehind, the actual offset may not point // at the markup character. Find it in the matched string. 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; // Find the matching closing token. - var end = re_end.exec(line); + var end = re_end ? re_end.exec(line) : null; if (end == null) { break; } @@ -449,12 +445,13 @@ Sample JSON representation of the text above: parse: function(content) { // Split text into lines. It makes further processing easier. var lines = content.split(/\r?\n/); - // Holds formatted lines - var blocks = []; + // Holds entities referenced from text var entityMap = []; var entityIndex = {}; - // + + // Processing lines one by one, hold intermediate result in blx. + var blx = []; lines.map(function(line) { var spans = []; var entities = []; @@ -489,8 +486,8 @@ Sample JSON representation of the text above: // Extract entities from the cleaned up string. entities = extractEntities(block.txt); - var ranges = []; if (entities.length > 0) { + var ranges = []; for (var i in entities) { // {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name} var ent = entities[i]; @@ -505,12 +502,42 @@ Sample JSON representation of the text above: block.ent = ranges; } - blocks.push(block); + blx.push(block); }); - var result = {blocks: blocks}; - if (entityMap.length > 0) { - result.refs = entityMap; + var result = {txt: ""}; + + // 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 0) { + result.refs = entityMap; + } } return result; }, @@ -526,46 +553,39 @@ Sample JSON representation of the text above: * @return HTML-representation of content */ unsafeToHTML: function(content) { - var {blocks} = content; - var {refs} = content; + var {txt, fmt, refs} = content; - if (!blocks) { - return ''; - } - - var text = []; - for (var i in blocks) { - 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 markup = []; + if (fmt) { + for (var i in fmt) { + var range = fmt[i]; + var tp = range.tp, data; + if (!tp) { var entity = refs[range.key]; - markup.push({idx: range.at, what: DECORATORS[entity.tp].open(entity.data)}); - markup.push({idx: range.at + range.len, what: DECORATORS[entity.tp].close(entity.data)}); + if (entity) { + 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("
"); + + 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 */ format: function(content, formatter, context) { - var {blocks} = content; - var {refs} = content; + var {txt, fmt, refs} = content; - if (!blocks) { - return []; + if (!fmt) { + return [txt]; } - var result = []; + var spans = [].concat(fmt); - // Process blocks one by one - for (var i = 0; i < blocks.length; i++) { - var block = blocks[i]; - var spans = []; - - // Merge markup ranges and sort in ascending order. - if (block.fmt) { - spans = spans.concat(block.fmt); + // Soft spans first by start index (asc) then by length (desc). + spans.sort(function(a, b) { + if (a.at - b.at == 0) { + return b.len - a.len; // longer one comes first (<0) } - if (block.ent) { - spans = spans.concat(block.ent); + return a.at - b.at; + }); + + // 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). - 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; + return forEach(txt, 0, txt.length, spans, formatter, context); }, /** * 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. */ toPlainText: function(content) { - var {blocks} = content; - - 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; + return content.text; }, /** * Returns true if content has no markup and no entities. * * @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. */ isPlainText: function(content, noLineBreaks) { - if (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; + return !(content.fmt || content.refs); }, + /** + * Get HTML tag for a given two-letter style name + * @param {string} style - two-letter style, like ST or LN + * @returns + */ 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) { if (data && DECORATORS[style]) { return DECORATORS[style].props(data); } return undefined; + }, + + /** + * Content MIME type. + * @returns string suitabe for HTTP Content-Type field. + */ + getContentType: function() { + return "text/x-drafty"; } }; diff --git a/index.html b/index.html index 109a3b62..277dd5a5 100644 --- a/index.html +++ b/index.html @@ -2030,6 +2030,8 @@ var elementKey = 0; var formatter = function(style, data, values) { + // console.log("El: " + style + ", data: " + JSON.stringify(data) + ", values: " + JSON.stringify(values)); + elementKey += 1; var el = Drafty.tagName(style); if (el) { @@ -2049,14 +2051,8 @@ (this.props.sequence === "single" || this.props.sequence === "last")); 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)); - } 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") ?
: line; - }); } return ( @@ -2655,7 +2651,7 @@ topic.publish(msg); } else { // 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.