diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index e5b90484010..62febca1a7a 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -475,7 +475,7 @@ Discourse.Dialect = { **/ replaceBlock: function(args) { - this.registerBlock(args.start.toString(), function(block, next) { + var fn = function(block, next) { var linebreaks = dialect.options.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks; @@ -565,7 +565,13 @@ Discourse.Dialect = { var emitterResult = args.emitter.call(this, contentBlocks, match, dialect.options); if (emitterResult) { result.push(emitterResult); } return result; - }); + }; + + if (args.priority) { + fn.priority = args.priority; + } + + this.registerBlock(args.start.toString(), fn); }, /** diff --git a/app/assets/javascripts/discourse/dialects/table_dialect.js b/app/assets/javascripts/discourse/dialects/table_dialect.js new file mode 100644 index 00000000000..d73393023e2 --- /dev/null +++ b/app/assets/javascripts/discourse/dialects/table_dialect.js @@ -0,0 +1,52 @@ +var flattenBlocks = function(blocks) { + var result = ""; + blocks.forEach(function(b) { + result += b; + if (b.trailing) { result += b.trailing; } + }); + + // bypass newline insertion + return result.replace(/[\n\r]/g, " "); +}; + +var emitter = function(contents) { + // TODO event should be fired when sanitizer loads + if (window.html4 && window.html4.ELEMENTS.td !== 1) { + window.html4.ELEMENTS.table = 0; + window.html4.ELEMENTS.tbody = 1; + window.html4.ELEMENTS.td = 1; + window.html4.ELEMENTS.thead = 1; + window.html4.ELEMENTS.th = 1; + window.html4.ELEMENTS.tr = 1; + } + return ['table', {"class": "md-table"}, flattenBlocks.apply(this, [contents])]; +}; + +var tableBlock = { + start: /()([\S\s]*)/igm, + stop: /<\/table>/igm, + rawContents: true, + emitter: emitter, + priority: 1 +}; + +var init = function(){ + if (Discourse.SiteSettings.allow_html_tables) { + Discourse.Markdown.whiteListTag("table"); + Discourse.Markdown.whiteListTag("table", "class", "md-table"); + Discourse.Markdown.whiteListTag("tbody"); + Discourse.Markdown.whiteListTag("thead"); + Discourse.Markdown.whiteListTag("tr"); + Discourse.Markdown.whiteListTag("th"); + Discourse.Markdown.whiteListTag("td"); + Discourse.Dialect.replaceBlock(tableBlock); + + } +}; + +if (Discourse.SiteSettings) { + init(); +} else { + Discourse.initializer({initialize: init, name: 'enable-html-tables'}); +} + diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 7e2444b2cc3..ff80230d2d2 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -243,4 +243,20 @@ blockquote > *:last-child { } +table.md-table { + thead { + border-bottom: 2px solid lighten($primary, 80%); + th { + text-align: left; + padding-bottom: 2px; + } + } + + td,th { + padding: 3px 3px 3px 10px; + } + tr { + border-bottom: 1px solid lighten($primary, 80%); + } +} diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4b01b8b3e81..f4d2c718ca5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -842,6 +842,7 @@ en: flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." + allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whiteliseted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" diff --git a/config/site_settings.yml b/config/site_settings.yml index eb5755b1d99..716a89332a9 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -386,6 +386,9 @@ posting: traditional_markdown_linebreaks: client: true default: false + allow_html_tables: + client: true + default: false suppress_reply_directly_below: client: true default: true diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 9416d0a681d..de5e3fbb9cc 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -331,4 +331,30 @@ describe PrettyText do expect(PrettyText.cook(raw)).to match_html(cooked) end + describe 'tables' do + before do + PrettyText.reset_context + end + + after do + PrettyText.reset_context + end + + it 'allows table html' do + SiteSetting.allow_html_tables = true + PrettyText.reset_context + table = "
\n
test
a
" + match = "
test
a
" + expect(PrettyText.cook(table)).to match_html(match) + + end + + it 'allows no tables when not enabled' do + SiteSetting.allow_html_tables = false + table = "
test
a
" + expect(PrettyText.cook(table)).to match_html("") + end + + end + end diff --git a/vendor/assets/javascripts/better_markdown.js b/vendor/assets/javascripts/better_markdown.js index 7a9e74b8c23..a445aacec5c 100644 --- a/vendor/assets/javascripts/better_markdown.js +++ b/vendor/assets/javascripts/better_markdown.js @@ -318,13 +318,26 @@ // Build default order from insertion order. Markdown.buildBlockOrder = function(d) { - var ord = []; + var ord = [[]]; for ( var i in d ) { if ( i === "__order__" || i === "__call__" ) continue; - ord.push( i ); + + var priority = d[i].priority || 0; + ord[priority] = ord[priority] || []; + ord[priority].push( i ); } - d.__order__ = ord; + + var flattend = []; + for (i=ord.length-1; i>=0; i--){ + if (ord[i]) { + for (var j=0; j