2013-03-06 04:39:21 +08:00
|
|
|
/*global Markdown:true */
|
2013-03-06 03:33:27 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
Contains methods to help us with markdown formatting.
|
|
|
|
|
|
|
|
@class Markdown
|
|
|
|
@namespace Discourse
|
|
|
|
@module Discourse
|
|
|
|
**/
|
|
|
|
Discourse.Markdown = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
Convert a raw string to a cooked markdown string.
|
|
|
|
|
|
|
|
@method cook
|
|
|
|
@param {String} raw the raw string we want to apply markdown to
|
|
|
|
@param {Object} opts the options for the rendering
|
2013-03-06 04:39:21 +08:00
|
|
|
@return {String} the cooked markdown string
|
2013-03-06 03:33:27 +08:00
|
|
|
**/
|
|
|
|
cook: function(raw, opts) {
|
|
|
|
if (!opts) opts = {};
|
|
|
|
|
|
|
|
// Make sure we've got a string
|
|
|
|
if (!raw) return "";
|
|
|
|
if (raw.length === 0) return "";
|
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
return this.markdownConverter(opts).makeHtml(raw);
|
2013-03-06 03:33:27 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2013-03-06 04:39:21 +08:00
|
|
|
Creates a new pagedown markdown editor, supplying i18n translations.
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
@method createEditor
|
|
|
|
@param {Object} converterOptions custom options for our markdown converter
|
|
|
|
@return {Markdown.Editor} the editor instance
|
2013-03-06 03:33:27 +08:00
|
|
|
**/
|
2013-03-06 04:39:21 +08:00
|
|
|
createEditor: function(converterOptions) {
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
if (!converterOptions) converterOptions = {};
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
// By default we always sanitize content in the editor
|
|
|
|
converterOptions.sanitize = true;
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
var markdownConverter = Discourse.Markdown.markdownConverter(converterOptions);
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
var editorOptions = {
|
|
|
|
strings: {
|
2013-07-09 07:32:16 +08:00
|
|
|
bold: I18n.t("composer.bold_title") + " <strong> Ctrl+B",
|
|
|
|
boldexample: I18n.t("composer.bold_text"),
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
italic: I18n.t("composer.italic_title") + " <em> Ctrl+I",
|
|
|
|
italicexample: I18n.t("composer.italic_text"),
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
link: I18n.t("composer.link_title") + " <a> Ctrl+L",
|
|
|
|
linkdescription: I18n.t("composer.link_description"),
|
|
|
|
linkdialog: "<p><b>" + I18n.t("composer.link_dialog_title") + "</b></p><p>http://example.com/ \"" +
|
|
|
|
I18n.t("composer.link_optional_text") + "\"</p>",
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
quote: I18n.t("composer.quote_title") + " <blockquote> Ctrl+Q",
|
|
|
|
quoteexample: I18n.t("composer.quote_text"),
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
code: I18n.t("composer.code_title") + " <pre><code> Ctrl+K",
|
|
|
|
codeexample: I18n.t("composer.code_text"),
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
image: I18n.t("composer.image_title") + " <img> Ctrl+G",
|
|
|
|
imagedescription: I18n.t("composer.image_description"),
|
|
|
|
imagedialog: "<p><b>" + I18n.t("composer.image_dialog_title") + "</b></p><p>http://example.com/images/diagram.jpg \"" +
|
|
|
|
I18n.t("composer.image_optional_text") + "\"<br><br>" + I18n.t("composer.image_hosting_hint") + "</p>",
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
olist: I18n.t("composer.olist_title") + " <ol> Ctrl+O",
|
|
|
|
ulist: I18n.t("composer.ulist_title") + " <ul> Ctrl+U",
|
|
|
|
litem: I18n.t("composer.list_item"),
|
2013-03-06 04:39:21 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
heading: I18n.t("composer.heading_title") + " <h1>/<h2> Ctrl+H",
|
|
|
|
headingexample: I18n.t("composer.heading_text"),
|
2013-03-06 04:39:21 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
hr: I18n.t("composer.hr_title") + " <hr> Ctrl+R",
|
2013-03-06 04:39:21 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
undo: I18n.t("composer.undo_title") + " - Ctrl+Z",
|
|
|
|
redo: I18n.t("composer.redo_title") + " - Ctrl+Y",
|
|
|
|
redomac: I18n.t("composer.redo_title") + " - Ctrl+Shift+Z",
|
2013-03-06 04:39:21 +08:00
|
|
|
|
2013-07-09 07:32:16 +08:00
|
|
|
help: I18n.t("composer.help")
|
2013-03-06 04:39:21 +08:00
|
|
|
}
|
2013-03-06 03:33:27 +08:00
|
|
|
};
|
|
|
|
|
2013-03-06 04:39:21 +08:00
|
|
|
return new Markdown.Editor(markdownConverter, undefined, editorOptions);
|
2013-03-06 03:33:27 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Creates a Markdown.Converter that we we can use for formatting
|
|
|
|
|
|
|
|
@method markdownConverter
|
|
|
|
@param {Object} opts the converting options
|
|
|
|
**/
|
|
|
|
markdownConverter: function(opts) {
|
2013-03-06 04:39:21 +08:00
|
|
|
if (!opts) opts = {};
|
|
|
|
|
|
|
|
var converter = new Markdown.Converter();
|
|
|
|
var mentionLookup = opts.mentionLookup || Discourse.Mention.lookupCache;
|
2013-03-06 03:33:27 +08:00
|
|
|
|
2013-06-27 06:41:48 +08:00
|
|
|
var quoteTemplate = null, urlsTemplate = null;
|
2013-04-10 05:31:59 +08:00
|
|
|
|
2013-03-06 03:33:27 +08:00
|
|
|
// Before cooking callbacks
|
2013-07-04 06:25:38 +08:00
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
|
|
|
// If a user puts text right up against a quote, make sure the spacing is equivalnt to a new line
|
|
|
|
return text.replace(/\[\/quote\]/, "[/quote]\n");
|
|
|
|
});
|
|
|
|
|
2013-03-06 03:33:27 +08:00
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
2013-03-06 04:39:21 +08:00
|
|
|
Discourse.Markdown.trigger('beforeCook', { detail: text, opts: opts });
|
|
|
|
return Discourse.Markdown.textResult || text;
|
2013-03-06 03:33:27 +08:00
|
|
|
});
|
|
|
|
|
2013-04-10 05:31:59 +08:00
|
|
|
// Extract quotes so their contents are not passed through markdown.
|
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
2013-06-25 23:13:41 +08:00
|
|
|
var extracted = Discourse.BBCode.extractQuotes(text);
|
2013-04-10 05:31:59 +08:00
|
|
|
quoteTemplate = extracted.template;
|
|
|
|
return extracted.text;
|
|
|
|
});
|
|
|
|
|
2013-06-27 06:41:48 +08:00
|
|
|
// Extract urls in BBCode tags so they are not passed through markdown.
|
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
|
|
|
var extracted = Discourse.BBCode.extractUrls(text);
|
|
|
|
urlsTemplate = extracted.template;
|
|
|
|
return extracted.text;
|
|
|
|
});
|
|
|
|
|
2013-03-06 03:33:27 +08:00
|
|
|
// Support autolinking of www.something.com
|
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
|
|
|
return text.replace(/(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, function(full, _, rest) {
|
|
|
|
return " <a href=\"http://" + rest + "\">" + rest + "</a>";
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// newline prediction in trivial cases
|
2013-06-21 23:36:33 +08:00
|
|
|
var linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
|
|
|
|
if (!linebreaks) {
|
2013-03-06 03:33:27 +08:00
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
|
|
|
return text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) {
|
2013-03-06 04:39:21 +08:00
|
|
|
if (t.match(/\n{2}/gim)) return t;
|
2013-03-06 03:33:27 +08:00
|
|
|
return t.replace("\n", " \n");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// github style fenced code
|
|
|
|
converter.hooks.chain("preConversion", function(text) {
|
|
|
|
return text.replace(/^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, function(wholeMatch, m1, m2) {
|
2013-03-06 04:39:21 +08:00
|
|
|
var escaped = Handlebars.Utils.escapeExpression(m2);
|
2013-03-06 03:33:27 +08:00
|
|
|
return "<pre><code class='" + (m1 || 'lang-auto') + "'>" + escaped + "</code></pre>";
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
converter.hooks.chain("postConversion", function(text) {
|
|
|
|
if (!text) return "";
|
|
|
|
|
2013-04-27 06:11:26 +08:00
|
|
|
// don't do @username mentions inside <pre> or <code> blocks
|
2013-05-16 07:59:07 +08:00
|
|
|
text = text.replace(/<(pre|code)>([\s\S](?!<(pre|code)>))*?@([\s\S](?!<(pre|code)>))*?<\/(pre|code)>/gi, function(m) {
|
|
|
|
return m.replace(/@/g, '@');
|
2013-03-06 03:33:27 +08:00
|
|
|
});
|
|
|
|
|
2013-04-27 05:34:03 +08:00
|
|
|
// add @username mentions, if valid; must be bounded on left and right by non-word characters
|
|
|
|
text = text.replace(/(\W)(@[A-Za-z0-9][A-Za-z0-9_]{2,14})(?=\W)/g, function(x, pre, name) {
|
2013-03-06 03:33:27 +08:00
|
|
|
if (mentionLookup(name.substr(1))) {
|
2013-03-14 20:01:52 +08:00
|
|
|
return pre + "<a href='" + Discourse.getURL("/users/") + (name.substr(1).toLowerCase()) + "' class='mention'>" + name + "</a>";
|
2013-03-06 03:33:27 +08:00
|
|
|
} else {
|
2013-03-06 04:39:21 +08:00
|
|
|
return pre + "<span class='mention'>" + name + "</span>";
|
2013-03-06 03:33:27 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// a primitive attempt at oneboxing, this regex gives me much eye sores
|
|
|
|
text = text.replace(/(<li>)?((<p>|<br>)[\s\n\r]*)(<a href=["]([^"]+)[^>]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|<br>))/gi, function() {
|
|
|
|
// We don't onebox items in a list
|
2013-03-06 04:39:21 +08:00
|
|
|
if (arguments[1]) return arguments[0];
|
|
|
|
var url = arguments[5];
|
|
|
|
var onebox;
|
|
|
|
|
2013-03-06 03:33:27 +08:00
|
|
|
if (Discourse && Discourse.Onebox) {
|
|
|
|
onebox = Discourse.Onebox.lookupCache(url);
|
|
|
|
}
|
2013-06-11 04:48:50 +08:00
|
|
|
if (onebox && onebox.trim().length > 0) {
|
2013-03-06 03:33:27 +08:00
|
|
|
return arguments[2] + onebox;
|
|
|
|
} else {
|
|
|
|
return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return(text);
|
|
|
|
});
|
|
|
|
|
|
|
|
converter.hooks.chain("postConversion", function(text) {
|
2013-04-10 05:31:59 +08:00
|
|
|
// reapply quotes
|
2013-06-27 06:41:48 +08:00
|
|
|
if (quoteTemplate) { text = quoteTemplate(text); }
|
|
|
|
// reapply urls
|
|
|
|
if (urlsTemplate) { text = urlsTemplate(text); }
|
|
|
|
// format with BBCode
|
2013-06-25 23:13:41 +08:00
|
|
|
return Discourse.BBCode.format(text, opts);
|
2013-03-06 03:33:27 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (opts.sanitize) {
|
|
|
|
converter.hooks.chain("postConversion", function(text) {
|
2013-03-06 04:39:21 +08:00
|
|
|
if (!window.sanitizeHtml) return "";
|
|
|
|
return window.sanitizeHtml(text);
|
2013-03-06 03:33:27 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return converter;
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
RSVP.EventTarget.mixin(Discourse.Markdown);
|