Implemented basic text formatting package using a modfied version of the marked js library. Supports *bold*, _italics_, and code

Этот коммит содержится в:
Reed Garmsen
2015-07-08 11:34:34 -07:00
родитель 95e6626f9f
Коммит d293bc0b79
21 изменённых файлов: 2387 добавлений и 42 удалений

Просмотреть файл

@@ -156,7 +156,7 @@ module.exports = React.createClass({
}
var channel = this.state.channel;
var description = utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true});
var description = utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true, noTextFormatting: true});
var popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>);
var channelTitle = channel.display_name;
var currentId = UserStore.getCurrentId();

Просмотреть файл

@@ -184,6 +184,7 @@ module.exports = React.createClass({
</div>
);
}
var allowTextFormatting = config.AllowTextFormatting;
var postError = null;
if (this.state.postError) {
@@ -204,6 +205,10 @@ module.exports = React.createClass({
if (postError) {
postFooterClassName += ' has-error';
}
var extraInfo = <MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} />;
if (this.state.messageText.split(' ').length > 1 && allowTextFormatting) {
extraInfo = <span className='msg-format-help'>_<em>italics</em>_ *<strong>bold</strong>* `<code className='code-info'>code</code>`</span>;
}
return (
<form onSubmit={this.handleSubmit}>
@@ -224,7 +229,7 @@ module.exports = React.createClass({
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError} />
</div>
<MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} />
{extraInfo}
<div className={postFooterClassName}>
<input type='button' className='btn btn-primary comment-btn pull-right' value='Add Comment' onClick={this.handleSubmit} />
{postError}

Просмотреть файл

@@ -224,6 +224,7 @@ module.exports = React.createClass({
</div>
);
}
var allowTextFormatting = config.AllowTextFormatting;
var postError = null;
if (this.state.postError) {
@@ -244,6 +245,10 @@ module.exports = React.createClass({
if (postError) {
postFooterClassName += ' has-error';
}
var extraInfo = <MsgTyping channelId={this.state.channel_id} parentId='' />;
if (this.state.messageText.split(' ').length > 1 && allowTextFormatting) {
extraInfo = <span className='msg-typing'>_<em>italics</em>_ *<strong>bold</strong>* `<code className='code-info'>code</code>`</span>;
}
return (
<form id='create_post' ref='topDiv' role='form' onSubmit={this.handleSubmit}>
@@ -268,7 +273,7 @@ module.exports = React.createClass({
{postError}
{serverError}
{preview}
<MsgTyping channelId={this.state.channelId} parentId=''/>
{extraInfo}
</div>
</div>
</form>

Просмотреть файл

@@ -4,6 +4,7 @@
var FileAttachmentList = require('./file_attachment_list.jsx');
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var formatText = require('../../static/js/marked/lib/marked.js');
module.exports = React.createClass({
componentWillReceiveProps: function(nextProps) {
@@ -19,6 +20,7 @@ module.exports = React.createClass({
var filenames = this.props.post.filenames;
var parentPost = this.props.parentPost;
var inner = utils.textToJsx(this.state.message);
var allowTextFormatting = config.AllowTextFormatting;
var comment = "";
var reply = "";
@@ -50,11 +52,20 @@ module.exports = React.createClass({
}
}
comment = (
<p className="post-link">
<span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{message}</a></span>
</p>
);
if (allowTextFormatting) {
message = formatText(message, {sanitize: true, mangle: false, gfm: true, breaks: true, tables: false, smartypants: true, renderer: utils.customMarkedRenderer({disable: true})});
comment = (
<p className="post-link">
<span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick} dangerouslySetInnerHTML={{__html: message}} /></span>
</p>
);
} else {
comment = (
<p className="post-link">
<span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{message}</a></span>
</p>
);
}
postClass += " post-comment";
}
@@ -67,7 +78,11 @@ module.exports = React.createClass({
return (
<div className="post-body">
{ comment }
{allowTextFormatting ?
<div key={post.id+"_message"} className={postClass}><span>{inner}</span></div>
:
<p key={post.id+"_message"} className={postClass}><span>{inner}</span></p>
}
{ filenames && filenames.length > 0 ?
<FileAttachmentList
filenames={filenames}

Просмотреть файл

@@ -197,7 +197,10 @@ module.exports = React.createClass({
var post = post_list.posts[msg.props.post_id];
post.message = msg.props.message;
post.lastEditDate = Date.now();
post_list.posts[post.id] = post;
this.setState({ post_list: post_list });
PostStore.storePosts(msg.channel_id, post_list);
@@ -430,8 +433,12 @@ module.exports = React.createClass({
// it is the last comment if it is last post in the channel or the next post has a different root post
var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i-1]].root_id != post.root_id);
var postKey = post.id;
if (post.lastEditDate != undefined)
postKey += post.lastEditDate;
var postCtl = (
<Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id}
<Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={postKey}
posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment}
/>
);

Просмотреть файл

@@ -56,6 +56,7 @@ RhsHeaderPost = React.createClass({
RootPost = React.createClass({
render: function() {
var allowTextFormatting = config.AllowTextFormatting;
var post = this.props.post;
var message = utils.textToJsx(post.message);
var isOwner = UserStore.getCurrentId() == post.user_id;
@@ -76,6 +77,11 @@ RootPost = React.createClass({
channelName = (channel.type === 'D') ? "Private Message" : channel.display_name;
}
var messageHolder = <p>{message}</p>;
if (allowTextFormatting) {
messageHolder = <div>{message}</div>;
}
return (
<div className={"post post--root " + currentUserCss}>
<div className="post-right-channel__name">{ channelName }</div>
@@ -101,7 +107,7 @@ RootPost = React.createClass({
</li>
</ul>
<div className="post-body">
<p>{message}</p>
{messageHolder}
{ post.filenames && post.filenames.length > 0 ?
<FileAttachmentList
filenames={post.filenames}
@@ -119,6 +125,7 @@ RootPost = React.createClass({
CommentPost = React.createClass({
render: function() {
var allowTextFormatting = config.AllowTextFormatting;
var post = this.props.post;
var commentClass = "post";
@@ -138,6 +145,11 @@ CommentPost = React.createClass({
var message = utils.textToJsx(post.message);
var timestamp = UserStore.getCurrentUser().update_at;
var messageHolder = <p>{message}</p>;
if (allowTextFormatting) {
messageHolder = <div>{message}</div>;
}
return (
<div className={commentClass + " " + currentUserCss}>
<div className="post-profile-img__container">
@@ -160,7 +172,7 @@ CommentPost = React.createClass({
</li>
</ul>
<div className="post-body">
<p>{message}</p>
{messageHolder}
{ post.filenames && post.filenames.length > 0 ?
<FileAttachmentList
filenames={post.filenames}
@@ -273,11 +285,20 @@ module.exports = React.createClass({
root_post = post_list.posts[selected_post.root_id];
}
var rootPostKey = root_post.id
if (root_post.lastEditDate != undefined)
rootPostKey += root_post.lastEditDate;
var posts_array = [];
for (var postId in post_list.posts) {
var cpost = post_list.posts[postId];
if (cpost.root_id == root_post.id) {
var cpostKey = cpost.id
if (cpost.lastEditDate != undefined)
cpostKey += cpost.lastEditDate;
cpost.cpostKey = cpostKey;
posts_array.push(cpost);
}
}
@@ -300,10 +321,10 @@ module.exports = React.createClass({
<div className="sidebar-right__body">
<RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} />
<div className="post-right__scroll">
<RootPost post={root_post} commentCount={posts_array.length}/>
<RootPost key={rootPostKey} post={root_post} commentCount={posts_array.length}/>
<div className="post-right-comments-container">
{ posts_array.map(function(cpost) {
return <CommentPost ref={cpost.id} key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } />
return <CommentPost ref={cpost.id} key={cpost.cpostKey} post={cpost} selected={ (cpost.id == selected_post.id) } />
})}
</div>
<div className="post-create__container">

Просмотреть файл

@@ -84,6 +84,8 @@ var SearchItem = React.createClass({
channelName = (channel.type === 'D') ? "Private Message" : channel.display_name;
}
var searchItemKey = Date.now().toString();
return (
<div className="search-item-container post" onClick={this.handleClick}>
<div className="search-channel__name">{ channelName }</div>
@@ -99,7 +101,7 @@ var SearchItem = React.createClass({
</time>
</li>
</ul>
<div className="search-item-snippet"><span>{message}</span></div>
<div key={this.props.key + searchItemKey} className="search-item-snippet"><span>{message}</span></div>
</div>
</div>
);
@@ -131,6 +133,7 @@ module.exports = React.createClass({
if (this.isMounted()) {
var newState = getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
newState.last_edit_time = Date.now();
this.setState(newState);
}
}
@@ -152,6 +155,11 @@ module.exports = React.createClass({
var noResults = (!results || !results.order || !results.order.length);
var searchTerm = PostStore.getSearchTerm();
var searchItemKey = "";
if (this.state.last_edit_time) {
searchItemKey += this.state.last_edit_time.toString();
}
return (
<div className="sidebar--right__content">
<div className="search-bar__container sidebar--right__search-header">{searchForm}</div>
@@ -162,7 +170,7 @@ module.exports = React.createClass({
{ noResults ? <div className="sidebar--right__subheader">No results</div>
: results.order.map(function(id) {
var post = results.posts[id];
return <SearchItem key={post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} />
return <SearchItem key={searchItemKey + post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} />
}, this)
}

Просмотреть файл

@@ -11,6 +11,7 @@ var BrowserStore = require('../stores/browser_store.jsx');
var utils = require('../utils/utils.jsx');
var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx');
var formatText = require('../../static/js/marked/lib/marked.js');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -207,6 +208,11 @@ module.exports = React.createClass({
utils.notifyMe(title, username + ' did something new', channel);
}
} else {
var allowTextFormatting = config.AllowTextFormatting;
if (allowTextFormatting) {
notifyText = formatText(notifyText, {sanitize: false, mangle: false, gfm: true, breaks: true, tables: false, smartypants: true, renderer: utils.customMarkedRenderer({disable: true})});
}
notifyText = utils.replaceHtmlEntities(notifyText);
utils.notifyMe(title, username + ' wrote: ' + notifyText, channel);
}
if (!user.notify_props || user.notify_props.desktop_sound === 'true') {

Просмотреть файл

@@ -9,6 +9,7 @@ var ActionTypes = Constants.ActionTypes;
var AsyncClient = require('./async_client.jsx');
var client = require('./client.jsx');
var Autolinker = require('autolinker');
var formatText = require('../../static/js/marked/lib/marked.js');
module.exports.isEmail = function(email) {
var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
@@ -95,9 +96,8 @@ module.exports.getCookie = function(name) {
if (parts.length == 2) return parts.pop().split(";").shift();
}
module.exports.notifyMe = function(title, body, channel) {
if ("Notification" in window && Notification.permission !== 'denied') {
if ("Notification" in window && Notification.permission !== 'denied') {
Notification.requestPermission(function (permission) {
if (Notification.permission !== permission) {
Notification.permission = permission;
@@ -385,11 +385,53 @@ module.exports.searchForTerm = function(term) {
});
}
/* Options:
- disable: Parses out *'s and other format specifiers in the text, but doesn't convert to html
*/
module.exports.customMarkedRenderer = function(options) {
var customTextRenderer = new formatText.Renderer();
if (options && options.disable) {
customTextRenderer.paragraph = function(text) {
return text + ' ';
};
customTextRenderer.strong = function(text) {
return text;
};
customTextRenderer.em = function(text) {
return text;
};
customTextRenderer.codespan = function(code) {
return code;
};
customTextRenderer.link = function(href, title, text) {
return href;
};
customTextRenderer.image = function(href, title, text) {
return href;
};
} else {
customTextRenderer.link = function(href, title, text) {
return href;
};
customTextRenderer.image = function(href, title, text) {
return href;
};
}
return customTextRenderer;
};
var oldExplicitMentionRegex = /(?:<mention>)([\s\S]*?)(?:<\/mention>)/g;
var puncStartRegex = /^((?![@#])\W)+/g;
var puncEndRegex = /(\W)+$/g;
var puncStartRegex = /^((?![@#])[^A-Za-z0-9_<>])+/g;
var puncEndRegex = /([^A-Za-z0-9_<>])+$/g;
var startTagRegex = /(<\s*\w.*?>)+/g;
var endTagRegex = /(<\s*\/\s*\w\s*.*?>|<\s*br\s*>)+/g;
module.exports.textToJsx = function(text, options) {
var useTextFormatting = config.AllowTextFormatting && (!options || !options.noTextFormatting);
if (useTextFormatting) {
text = formatText(text, {sanitize: true, mangle: false, gfm: true, breaks: true, tables: false, smartypants: true, renderer: module.exports.customMarkedRenderer()});
}
if (options && options['singleline']) {
var repRegex = new RegExp("\n", "g");
@@ -398,7 +440,7 @@ module.exports.textToJsx = function(text, options) {
var searchTerm = ""
if (options && options['searchTerm']) {
searchTerm = options['searchTerm'].toLowerCase()
searchTerm = options['searchTerm'].toLowerCase();
}
var mentionClass = "mention-highlight";
@@ -407,6 +449,8 @@ module.exports.textToJsx = function(text, options) {
}
var inner = [];
var codeFlag = false;
var codeString = '';
// Function specific regex
var hashRegex = /^href="#[^"]+"|(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
@@ -420,16 +464,45 @@ module.exports.textToJsx = function(text, options) {
var highlightSearchClass = "";
for (var z = 0; z < words.length; z++) {
var word = words[z];
var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
var trimWord;
if (useTextFormatting) {
trimWord = word.replace(endTagRegex, '').replace(startTagRegex, '').replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
} else {
trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
}
var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
var explicitMention = mentionRegex.exec(trimWord);
if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != "") {
var prefix;
var suffix;
var prefixSpan;
var suffixSpan;
if (useTextFormatting) {
prefix = (word.match(startTagRegex) ? word.match(startTagRegex) : "") + (word.replace(startTagRegex, '').match(puncStartRegex) ? word.replace(startTagRegex, '').match(puncStartRegex) : "");
suffix = (word.match(endTagRegex) ? word.match(endTagRegex) : "") + (word.replace(endTagRegex, '').match(puncEndRegex) ? word.replace(endTagRegex, '').match(puncEndRegex) : "");
prefixSpan = prefix ? (<span key={word+i+z+"pre_span"}><span dangerouslySetInnerHTML={{__html: prefix}} /></span>) : null;
suffixSpan = suffix ? (<span key={word+i+z+"suf_span"}><span dangerouslySetInnerHTML={{__html: suffix}} /> </span>) : (<span key={word+i+z+"suf_span"}> </span>);
} else {
prefix = word.match(puncStartRegex);
suffix = word.match(puncEndRegex);
prefixSpan = prefix ? (<span key={word+i+z+"pre_span"}>{prefix}</span>) : null;
suffixSpan = suffix ? (<span key={word+i+z+"suf_span"}>{suffix} </span>) : (<span key={word+i+z+"suf_span"}> </span>);
}
if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != "") {
highlightSearchClass = " search-highlight";
}
if (explicitMention &&
if (useTextFormatting && (codeFlag || word.indexOf('<code>') !== -1)) {
codeString += word + ' ';
codeFlag = true;
if (word.indexOf('</code>') !== -1) {
inner.push(<span key={word+i+z+"_span"} className={highlightSearchClass} dangerouslySetInnerHTML={{__html: codeString}} />);
codeString = '';
codeFlag = false;
}
} else if (explicitMention &&
(UserStore.getProfileByUsername(explicitMention[1]) ||
Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1))
{
@@ -437,60 +510,86 @@ module.exports.textToJsx = function(text, options) {
// do both a non-case sensitive and case senstive check
var mClass = implicitKeywords.indexOf('@'+name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@'+name) !== -1 ? mentionClass : "";
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
if (searchTerm === name) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(name)}>@{name}</a>{suffix} </span>);
if (useTextFormatting) {
prefixSpan ? inner.push(prefixSpan) : null;
inner.push(<span key={word+i+z+"word_span"}><a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(name)}>{"@" + name}</a></span>);
suffixSpan ? inner.push(suffixSpan) : null;
}
else
inner.push(<span key={name+i+z+"_span"}>{prefix}<a className={mClass + highlightSearchClass + " mention-link"} key={name+i+z+"_link"} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(name)}>@{name}</a>{suffix} </span>);
} else if (testUrlMatch(word).length) {
var match = testUrlMatch(word)[0];
var link = match.link;
var prefix = word.substring(0,word.indexOf(match.text));
var suffix = word.substring(word.indexOf(match.text)+match.text.length);
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_link"} className={"theme" + highlightSearchClass} target="_blank" href={link}>{match.text}</a>{suffix} </span>);
prefix = word.substring(0,word.indexOf(match.text));
suffix = word.substring(word.indexOf(match.text)+match.text.length);
prefixSpan = prefix ? (<span key={word+i+z+"pre_span"}><span dangerouslySetInnerHTML={{__html: prefix}} /></span>) : null;
suffixSpan = suffix ? (<span key={word+i+z+"suf_span"}><span dangerouslySetInnerHTML={{__html: suffix}} /> </span>) : <span key={word+i+z+"suf_span"}> </span>;
if (useTextFormatting) {
prefixSpan ? inner.push(prefixSpan) : null;
inner.push(<span key={word+i+z+"_span"}><a key={name+i+z+"_link"} className={"theme" + highlightSearchClass} target="_blank" href={link}>{match.text}</a></span>);
suffixSpan ? inner.push(suffixSpan) : null;
}
else
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_link"} className={"theme" + highlightSearchClass} target="_blank" href={link}>{match.text}</a>{suffix} </span>);
} else if (trimWord.match(hashRegex)) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
var mClass = implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1 ? mentionClass : "";
if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
if (useTextFormatting) {
prefixSpan ? inner.push(prefixSpan) : null;
inner.push(<span key={word+i+z+"_span"}><a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a></span>);
suffixSpan ? inner.push(suffixSpan) : null;
}
else
inner.push(<span key={word+i+z+"_span"}>{prefix}<a key={word+i+z+"_hash"} className={"theme " + mClass + highlightSearchClass} href="#" onClick={function(value) { return function() { module.exports.searchForTerm(value); } }(trimWord)}>{trimWord}</a>{suffix} </span>);
} else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
var suffix = word.match(puncEndRegex);
var prefix = word.match(puncStartRegex);
if (trimWord.charAt(0) === '@') {
if (searchTerm === trimWord.substring(1).toLowerCase()) {
highlightSearchClass = " search-highlight";
}
inner.push(<span key={word+i+z+"_span"} key={name+i+z+"_span"}>{prefix}<a className={mentionClass + highlightSearchClass} key={name+i+z+"_link"} href="#">{trimWord}</a>{suffix} </span>);
if (useTextFormatting) {
prefixSpan ? inner.push(prefixSpan) : null;
inner.push(<span key={word+i+z+"_span"} key={name+i+z+"_span"}><a className={mentionClass + highlightSearchClass} key={name+i+z+"_link"} href="#">{trimWord}</a></span>);
suffixSpan ? inner.push(suffixSpan) : null;
}
else
inner.push(<span key={word+i+z+"_span"} key={name+i+z+"_span"}>{prefix}<a className={mentionClass + highlightSearchClass} key={name+i+z+"_link"} href="#">{trimWord}</a>{suffix} </span>);
} else {
inner.push(<span key={word+i+z+"_span"}>{prefix}<span className={mentionClass + highlightSearchClass}>{module.exports.replaceHtmlEntities(trimWord)}</span>{suffix} </span>);
if (useTextFormatting) {
prefixSpan ? inner.push(prefixSpan) : null;
inner.push(<span key={word+i+z+"_span"}><span className={mentionClass + highlightSearchClass}>{trimWord}</span></span>);
suffixSpan ? inner.push(suffixSpan) : null;
}
else
inner.push(<span key={word+i+z+"_span"}>{prefix}<span className={mentionClass + highlightSearchClass}>{module.exports.replaceHtmlEntities(trimWord)}</span>{suffix} </span>);
}
} else if (word === "") {
// if word is empty dont include a span
} else {
inner.push(<span key={word+i+z+"_span"}><span className={highlightSearchClass}>{module.exports.replaceHtmlEntities(word)}</span> </span>);
if (useTextFormatting)
inner.push(<span key={word+i+z+"_span"} className={highlightSearchClass} dangerouslySetInnerHTML={{__html: word + ' '}} />);
else
inner.push(<span key={word+i+z+"_span"}><span className={highlightSearchClass}>{module.exports.replaceHtmlEntities(word)}</span> </span>);
}
highlightSearchClass = "";
}
if (i != lines.length-1)
if (!useTextFormatting && i != lines.length-1)
inner.push(<br key={"br_"+i+z}/>);
}
return inner;
}
};
module.exports.getFileType = function(ext) {
ext = ext.toLowerCase();

Просмотреть файл

@@ -194,11 +194,19 @@ body.ios {
}
}
.msg-typing {
padding-bottom: 5px;
min-height: 20px;
line-height: 18px;
display: inline-block;
font-size: 13px;
color: #777;
.code-info {
padding-left: 2px;
padding-right: 2px;
}
.preformatted-info {
background-color: #E3E8E3;
}
}
}
}

Просмотреть файл

@@ -33,6 +33,19 @@
float: left;
padding-top: 17px;
}
.msg-format-help {
min-height: 20px;
line-height: 18px;
display: inline-block;
font-size: 13px;
color: #555;
float: left;
padding-top: 17px;
.code-info {
padding-left: 1px;
padding-right: 1px;
}
}
.post-create-footer {
padding-top: 10px;
}

Просмотреть файл

@@ -15,6 +15,7 @@ var config = {
AllowInviteNames: true,
RequireInviteNames: false,
AllowSignupDomainsWizard: false,
AllowTextFormatting: true,
// Google Developer Key (for Youtube API links)
// Leave blank to disable

19
web/static/js/marked/LICENSE Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

12
web/static/js/marked/Makefile Обычный файл
Просмотреть файл

@@ -0,0 +1,12 @@
all:
@cp lib/marked.js marked.js
@uglifyjs --comments '/\*[^\0]+?Copyright[^\0]+?\*/' -o marked.min.js lib/marked.js
clean:
@rm marked.js
@rm marked.min.js
bench:
@node test --bench
.PHONY: clean all

406
web/static/js/marked/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,406 @@
# marked
> A full-featured markdown parser and compiler, written in JavaScript. Built
> for speed.
[![NPM version](https://badge.fury.io/js/marked.png)][badge]
## Install
``` bash
npm install marked --save
```
## Usage
Minimal usage:
```js
var marked = require('marked');
console.log(marked('I am using __markdown__.'));
// Outputs: <p>I am using <strong>markdown</strong>.</p>
```
Example setting options with default values:
```js
var marked = require('marked');
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
console.log(marked('I am using __markdown__.'));
```
### Browser
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
<script src="lib/marked.js"></script>
</head>
<body>
<div id="content"></div>
<script>
document.getElementById('content').innerHTML =
marked('# Marked in browser\n\nRendered by **marked**.');
</script>
</body>
</html>
```
## marked(markdownString [,options] [,callback])
### markdownString
Type: `string`
String of markdown source to be compiled.
### options
Type: `object`
Hash of options. Can also be set using the `marked.setOptions` method as seen
above.
### callback
Type: `function`
Function called when the `markdownString` has been fully parsed when using
async highlighting. If the `options` argument is omitted, this can be used as
the second argument.
## Options
### highlight
Type: `function`
A function to highlight code blocks. The first example below uses async highlighting with
[node-pygmentize-bundled][pygmentize], and the second is a synchronous example using
[highlight.js][highlight]:
```js
var marked = require('marked');
var markdownString = '```js\n console.log("hello"); \n```';
// Async highlighting with pygmentize-bundled
marked.setOptions({
highlight: function (code, lang, callback) {
require('pygmentize-bundled')({ lang: lang, format: 'html' }, code, function (err, result) {
callback(err, result.toString());
});
}
});
// Using async version of marked
marked(markdownString, function (err, content) {
if (err) throw err;
console.log(content);
});
// Synchronous highlighting with highlight.js
marked.setOptions({
highlight: function (code) {
return require('highlight.js').highlightAuto(code).value;
}
});
console.log(marked(markdownString));
```
#### highlight arguments
`code`
Type: `string`
The section of code to pass to the highlighter.
`lang`
Type: `string`
The programming language specified in the code block.
`callback`
Type: `function`
The callback function to call when using an async highlighter.
### renderer
Type: `object`
Default: `new Renderer()`
An object containing functions to render tokens to HTML.
#### Overriding renderer methods
The renderer option allows you to render tokens in a custom manner. Here is an
example of overriding the default heading token rendering by adding an embedded anchor tag like on GitHub:
```javascript
var marked = require('marked');
var renderer = new marked.Renderer();
renderer.heading = function (text, level) {
var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
return '<h' + level + '><a name="' +
escapedText +
'" class="anchor" href="#' +
escapedText +
'"><span class="header-link"></span></a>' +
text + '</h' + level + '>';
},
console.log(marked('# heading+', { renderer: renderer }));
```
This code will output the following HTML:
```html
<h1>
<a name="heading-" class="anchor" href="#heading-">
<span class="header-link"></span>
</a>
heading+
</h1>
```
#### Block level renderer methods
- code(*string* code, *string* language)
- blockquote(*string* quote)
- html(*string* html)
- heading(*string* text, *number* level)
- hr()
- list(*string* body, *boolean* ordered)
- listitem(*string* text)
- paragraph(*string* text)
- table(*string* header, *string* body)
- tablerow(*string* content)
- tablecell(*string* content, *object* flags)
`flags` has the following properties:
```js
{
header: true || false,
align: 'center' || 'left' || 'right'
}
```
#### Inline level renderer methods
- strong(*string* text)
- em(*string* text)
- codespan(*string* code)
- br()
- del(*string* text)
- link(*string* href, *string* title, *string* text)
- image(*string* href, *string* title, *string* text)
### gfm
Type: `boolean`
Default: `true`
Enable [GitHub flavored markdown][gfm].
### tables
Type: `boolean`
Default: `true`
Enable GFM [tables][tables].
This option requires the `gfm` option to be true.
### breaks
Type: `boolean`
Default: `false`
Enable GFM [line breaks][breaks].
This option requires the `gfm` option to be true.
### pedantic
Type: `boolean`
Default: `false`
Conform to obscure parts of `markdown.pl` as much as possible. Don't fix any of
the original markdown bugs or poor behavior.
### sanitize
Type: `boolean`
Default: `false`
Sanitize the output. Ignore any HTML that has been input.
### smartLists
Type: `boolean`
Default: `true`
Use smarter list behavior than the original markdown. May eventually be
default with the old behavior moved into `pedantic`.
### smartypants
Type: `boolean`
Default: `false`
Use "smart" typograhic punctuation for things like quotes and dashes.
## Access to lexer and parser
You also have direct access to the lexer and parser if you so desire.
``` js
var tokens = marked.lexer(text, options);
console.log(marked.parser(tokens));
```
``` js
var lexer = new marked.Lexer(options);
var tokens = lexer.lex(text);
console.log(tokens);
console.log(lexer.rules);
```
## CLI
``` bash
$ marked -o hello.html
hello world
^D
$ cat hello.html
<p>hello world</p>
```
## Philosophy behind marked
The point of marked was to create a markdown compiler where it was possible to
frequently parse huge chunks of markdown without having to worry about
caching the compiled output somehow...or blocking for an unnecesarily long time.
marked is very concise and still implements all markdown features. It is also
now fully compatible with the client-side.
marked more or less passes the official markdown test suite in its
entirety. This is important because a surprising number of markdown compilers
cannot pass more than a few tests. It was very difficult to get marked as
compliant as it is. It could have cut corners in several areas for the sake
of performance, but did not in order to be exactly what you expect in terms
of a markdown rendering. In fact, this is why marked could be considered at a
disadvantage in the benchmarks above.
Along with implementing every markdown feature, marked also implements [GFM
features][gfmf].
## Benchmarks
node v0.8.x
``` bash
$ node test --bench
marked completed in 3411ms.
marked (gfm) completed in 3727ms.
marked (pedantic) completed in 3201ms.
robotskirt completed in 808ms.
showdown (reuse converter) completed in 11954ms.
showdown (new converter) completed in 17774ms.
markdown-js completed in 17191ms.
```
__Marked is now faster than Discount, which is written in C.__
For those feeling skeptical: These benchmarks run the entire markdown test suite 1000 times. The test suite tests every feature. It doesn't cater to specific aspects.
### Pro level
You also have direct access to the lexer and parser if you so desire.
``` js
var tokens = marked.lexer(text, options);
console.log(marked.parser(tokens));
```
``` js
var lexer = new marked.Lexer(options);
var tokens = lexer.lex(text);
console.log(tokens);
console.log(lexer.rules);
```
``` bash
$ node
> require('marked').lexer('> i am using marked.')
[ { type: 'blockquote_start' },
{ type: 'paragraph',
text: 'i am using marked.' },
{ type: 'blockquote_end' },
links: {} ]
```
## Running Tests & Contributing
If you want to submit a pull request, make sure your changes pass the test
suite. If you're adding a new feature, be sure to add your own test.
The marked test suite is set up slightly strangely: `test/new` is for all tests
that are not part of the original markdown.pl test suite (this is where your
test should go if you make one). `test/original` is only for the original
markdown.pl tests. `test/tests` houses both types of tests after they have been
combined and moved/generated by running `node test --fix` or `marked --test
--fix`.
In other words, if you have a test to add, add it to `test/new/` and then
regenerate the tests with `node test --fix`. Commit the result. If your test
uses a certain feature, for example, maybe it assumes GFM is *not* enabled, you
can add `.nogfm` to the filename. So, `my-test.text` becomes
`my-test.nogfm.text`. You can do this with any marked option. Say you want
line breaks and smartypants enabled, your filename should be:
`my-test.breaks.smartypants.text`.
To run the tests:
``` bash
cd marked/
node test
```
### Contribution and License Agreement
If you contribute code to this project, you are implicitly allowing your code
to be distributed under the MIT license. You are also implicitly verifying that
all code is your original work. `</legalese>`
## License
Copyright (c) 2011-2014, Christopher Jeffrey. (MIT License)
See LICENSE for more info.
[gfm]: https://help.github.com/articles/github-flavored-markdown
[gfmf]: http://github.github.com/github-flavored-markdown/
[pygmentize]: https://github.com/rvagg/node-pygmentize-bundled
[highlight]: https://github.com/isagalaev/highlight.js
[badge]: http://badge.fury.io/js/marked
[tables]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#wiki-tables
[breaks]: https://help.github.com/articles/github-flavored-markdown#newlines

187
web/static/js/marked/bin/marked Исполняемый файл
Просмотреть файл

@@ -0,0 +1,187 @@
#!/usr/bin/env node
/**
* Marked CLI
* Copyright (c) 2011-2013, Christopher Jeffrey (MIT License)
*/
var fs = require('fs')
, util = require('util')
, marked = require('../');
/**
* Man Page
*/
function help() {
var spawn = require('child_process').spawn;
var options = {
cwd: process.cwd(),
env: process.env,
setsid: false,
customFds: [0, 1, 2]
};
spawn('man',
[__dirname + '/../man/marked.1'],
options);
}
/**
* Main
*/
function main(argv, callback) {
var files = []
, options = {}
, input
, output
, arg
, tokens
, opt;
function getarg() {
var arg = argv.shift();
if (arg.indexOf('--') === 0) {
// e.g. --opt
arg = arg.split('=');
if (arg.length > 1) {
// e.g. --opt=val
argv.unshift(arg.slice(1).join('='));
}
arg = arg[0];
} else if (arg[0] === '-') {
if (arg.length > 2) {
// e.g. -abc
argv = arg.substring(1).split('').map(function(ch) {
return '-' + ch;
}).concat(argv);
arg = argv.shift();
} else {
// e.g. -a
}
} else {
// e.g. foo
}
return arg;
}
while (argv.length) {
arg = getarg();
switch (arg) {
case '--test':
return require('../test').main(process.argv.slice());
case '-o':
case '--output':
output = argv.shift();
break;
case '-i':
case '--input':
input = argv.shift();
break;
case '-t':
case '--tokens':
tokens = true;
break;
case '-h':
case '--help':
return help();
default:
if (arg.indexOf('--') === 0) {
opt = camelize(arg.replace(/^--(no-)?/, ''));
if (!marked.defaults.hasOwnProperty(opt)) {
continue;
}
if (arg.indexOf('--no-') === 0) {
options[opt] = typeof marked.defaults[opt] !== 'boolean'
? null
: false;
} else {
options[opt] = typeof marked.defaults[opt] !== 'boolean'
? argv.shift()
: true;
}
} else {
files.push(arg);
}
break;
}
}
function getData(callback) {
if (!input) {
if (files.length <= 2) {
return getStdin(callback);
}
input = files.pop();
}
return fs.readFile(input, 'utf8', callback);
}
return getData(function(err, data) {
if (err) return callback(err);
data = tokens
? JSON.stringify(marked.lexer(data, options), null, 2)
: marked(data, options);
if (!output) {
process.stdout.write(data + '\n');
return callback();
}
return fs.writeFile(output, data, callback);
});
}
/**
* Helpers
*/
function getStdin(callback) {
var stdin = process.stdin
, buff = '';
stdin.setEncoding('utf8');
stdin.on('data', function(data) {
buff += data;
});
stdin.on('error', function(err) {
return callback(err);
});
stdin.on('end', function() {
return callback(null, buff);
});
try {
stdin.resume();
} catch (e) {
callback(e);
}
}
function camelize(text) {
return text.replace(/(\w)-(\w)/g, function(_, a, b) {
return a + b.toUpperCase();
});
}
/**
* Expose / Entry Point
*/
if (!module.parent) {
process.title = 'marked';
main(process.argv.slice(), function(err, code) {
if (err) throw err;
return process.exit(code || 0);
});
} else {
module.exports = main;
}

426
web/static/js/marked/doc/broken.md Обычный файл
Просмотреть файл

@@ -0,0 +1,426 @@
# Markdown is broken
I have a lot of scraps of markdown engine oddities that I've collected over the
years. What you see below is slightly messy, but it's what I've managed to
cobble together to illustrate the differences between markdown engines, and
why, if there ever is a markdown specification, it has to be absolutely
thorough. There are a lot more of these little differences I have documented
elsewhere. I know I will find them lingering on my disk one day, but until
then, I'll continue to add whatever strange nonsensical things I find.
Some of these examples may only mention a particular engine compared to marked.
However, the examples with markdown.pl could easily be swapped out for
discount, upskirt, or markdown.js, and you would very easily see even more
inconsistencies.
A lot of this was written when I was very unsatisfied with the inconsistencies
between markdown engines. Please excuse the frustration noticeable in my
writing.
## Examples of markdown's "stupid" list parsing
```
$ markdown.pl
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p><p>text</p></li>
</ul></p>
```
```
$ marked
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p>text</p>
</li>
</ul>
```
Which looks correct to you?
- - -
```
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
```
$ marked
* hello
> world
^D
<ul>
<li>hello<blockquote>
<p>world</p>
</blockquote>
</li>
</ul>
```
Again, which looks correct to you?
- - -
EXAMPLE:
```
$ markdown.pl
* hello
* world
* hi
code
^D
<ul>
<li>hello
<ul>
<li>world</li>
<li>hi
code</li>
</ul></li>
</ul>
```
The code isn't a code block even though it's after the bullet margin. I know,
lets give it two more spaces, effectively making it 8 spaces past the bullet.
```
$ markdown.pl
* hello
* world
* hi
code
^D
<ul>
<li>hello
<ul>
<li>world</li>
<li>hi
code</li>
</ul></li>
</ul>
```
And, it's still not a code block. Did you also notice that the 3rd item isn't
even its own list? Markdown screws that up too because of its indentation
unaware parsing.
- - -
Let's look at some more examples of markdown's list parsing:
```
$ markdown.pl
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p><p>text</p></li>
</ul></p>
```
Misnested tags.
```
$ marked
* item1
* item2
text
^D
<ul>
<li><p>item1</p>
<ul>
<li>item2</li>
</ul>
<p>text</p>
</li>
</ul>
```
Which looks correct to you?
- - -
```
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
More misnested tags.
```
$ marked
* hello
> world
^D
<ul>
<li>hello<blockquote>
<p>world</p>
</blockquote>
</li>
</ul>
```
Again, which looks correct to you?
- - -
# Why quality matters - Part 2
``` bash
$ markdown.pl
* hello
> world
^D
<p><ul>
<li>hello</p>
<blockquote>
<p>world</li>
</ul></p>
</blockquote>
```
``` bash
$ sundown # upskirt
* hello
> world
^D
<ul>
<li>hello
&gt; world</li>
</ul>
```
``` bash
$ marked
* hello
> world
^D
<ul><li>hello <blockquote><p>world</p></blockquote></li></ul>
```
Which looks correct to you?
- - -
See: https://github.com/evilstreak/markdown-js/issues/23
``` bash
$ markdown.pl # upskirt/markdown.js/discount
* hello
var a = 1;
* world
^D
<ul>
<li>hello
var a = 1;</li>
<li>world</li>
</ul>
```
``` bash
$ marked
* hello
var a = 1;
* world
^D
<ul><li>hello
<pre>code>var a = 1;</code></pre></li>
<li>world</li></ul>
```
Which looks more reasonable? Why shouldn't code blocks be able to appear in
list items in a sane way?
- - -
``` bash
$ markdown.js
<div>hello</div>
<span>hello</span>
^D
<p>&lt;div&gt;hello&lt;/div&gt;</p>
<p>&lt;span&gt;hello&lt;/span&gt;</p>
```
``` bash
$ marked
<div>hello</div>
<span>hello</span>
^D
<div>hello</div>
<p><span>hello</span>
</p>
```
- - -
See: https://github.com/evilstreak/markdown-js/issues/27
``` bash
$ markdown.js
[![an image](/image)](/link)
^D
<p><a href="/image)](/link">![an image</a></p>
```
``` bash
$ marked
[![an image](/image)](/link)
^D
<p><a href="/link"><img src="/image" alt="an image"></a>
</p>
```
- - -
See: https://github.com/evilstreak/markdown-js/issues/24
``` bash
$ markdown.js
> a
> b
> c
^D
<blockquote><p>a</p><p>bundefined&gt; c</p></blockquote>
```
``` bash
$ marked
> a
> b
> c
^D
<blockquote><p>a
</p></blockquote>
<blockquote><p>b
</p></blockquote>
<blockquote><p>c
</p></blockquote>
```
- - -
``` bash
$ markdown.pl
* hello
* world
how
are
you
* today
* hi
^D
<ul>
<li><p>hello</p>
<ul>
<li>world
how</li>
</ul>
<p>are
you</p>
<ul>
<li>today</li>
</ul></li>
<li>hi</li>
</ul>
```
``` bash
$ marked
* hello
* world
how
are
you
* today
* hi
^D
<ul>
<li><p>hello</p>
<ul>
<li><p>world
how</p>
<p>are
you</p>
</li>
<li><p>today</p>
</li>
</ul>
</li>
<li>hi</li>
</ul>
```

2
web/static/js/marked/doc/todo.md Обычный файл
Просмотреть файл

@@ -0,0 +1,2 @@
# Todo

1008
web/static/js/marked/lib/marked.js Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

91
web/static/js/marked/man/marked.1 Обычный файл
Просмотреть файл

@@ -0,0 +1,91 @@
.ds q \N'34'
.TH marked 1 "2014-01-31" "v0.3.1" "marked.js"
.SH NAME
marked \- a javascript markdown parser
.SH SYNOPSIS
.B marked
[\-o \fI<output>\fP] [\-i \fI<input>\fP] [\-\-help]
[\-\-tokens] [\-\-pedantic] [\-\-gfm]
[\-\-breaks] [\-\-tables] [\-\-sanitize]
[\-\-smart\-lists] [\-\-lang\-prefix \fI<prefix>\fP]
[\-\-no\-etc...] [\-\-silent] [\fIfilename\fP]
.SH DESCRIPTION
.B marked
is a full-featured javascript markdown parser, built for speed. It also includes
multiple GFM features.
.SH EXAMPLES
.TP
cat in.md | marked > out.html
.TP
echo "hello *world*" | marked
.TP
marked \-o out.html in.md \-\-gfm
.TP
marked \-\-output="hello world.html" \-i in.md \-\-no-breaks
.SH OPTIONS
.TP
.BI \-o,\ \-\-output\ [\fIoutput\fP]
Specify file output. If none is specified, write to stdout.
.TP
.BI \-i,\ \-\-input\ [\fIinput\fP]
Specify file input, otherwise use last argument as input file. If no input file
is specified, read from stdin.
.TP
.BI \-t,\ \-\-tokens
Output a token stream instead of html.
.TP
.BI \-\-pedantic
Conform to obscure parts of markdown.pl as much as possible. Don't fix original
markdown bugs.
.TP
.BI \-\-gfm
Enable github flavored markdown.
.TP
.BI \-\-breaks
Enable GFM line breaks. Only works with the gfm option.
.TP
.BI \-\-tables
Enable GFM tables. Only works with the gfm option.
.TP
.BI \-\-sanitize
Sanitize output. Ignore any HTML input.
.TP
.BI \-\-smart\-lists
Use smarter list behavior than the original markdown.
.TP
.BI \-\-lang\-prefix\ [\fIprefix\fP]
Set the prefix for code block classes.
.TP
.BI \-\-mangle
Mangle email addresses.
.TP
.BI \-\-no\-sanitize,\ \-no-etc...
The inverse of any of the marked options above.
.TP
.BI \-\-silent
Silence error output.
.TP
.BI \-h,\ \-\-help
Display help information.
.SH CONFIGURATION
For configuring and running programmatically.
.B Example
require('marked')('*foo*', { gfm: true });
.SH BUGS
Please report any bugs to https://github.com/chjj/marked.
.SH LICENSE
Copyright (c) 2011-2014, Christopher Jeffrey (MIT License).
.SH "SEE ALSO"
.BR markdown(1),
.BR node.js(1)

6
web/static/js/marked/marked.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны