remove old markdown engine work-in-progress

This commit is contained in:
Sam Saffron 2017-07-12 08:13:33 -04:00 committed by Sam
parent 24db001bfa
commit ee470b5317
62 changed files with 639 additions and 3061 deletions

View File

@ -1,15 +1,15 @@
//= require markdown-it.js
//= require ./pretty-text/engines/markdown-it/helpers
//= require ./pretty-text/engines/markdown-it/mentions
//= require ./pretty-text/engines/markdown-it/quotes
//= require ./pretty-text/engines/markdown-it/emoji
//= require ./pretty-text/engines/markdown-it/onebox
//= require ./pretty-text/engines/markdown-it/bbcode-block
//= require ./pretty-text/engines/markdown-it/bbcode-inline
//= require ./pretty-text/engines/markdown-it/code
//= require ./pretty-text/engines/markdown-it/category-hashtag
//= require ./pretty-text/engines/markdown-it/censored
//= require ./pretty-text/engines/markdown-it/table
//= require ./pretty-text/engines/markdown-it/paragraph
//= require ./pretty-text/engines/markdown-it/newline
//= require ./pretty-text/engines/markdown-it/html_img
//= require ./pretty-text/engines/discourse-markdown/helpers
//= require ./pretty-text/engines/discourse-markdown/mentions
//= require ./pretty-text/engines/discourse-markdown/quotes
//= require ./pretty-text/engines/discourse-markdown/emoji
//= require ./pretty-text/engines/discourse-markdown/onebox
//= require ./pretty-text/engines/discourse-markdown/bbcode-block
//= require ./pretty-text/engines/discourse-markdown/bbcode-inline
//= require ./pretty-text/engines/discourse-markdown/code
//= require ./pretty-text/engines/discourse-markdown/category-hashtag
//= require ./pretty-text/engines/discourse-markdown/censored
//= require ./pretty-text/engines/discourse-markdown/table
//= require ./pretty-text/engines/discourse-markdown/paragraph
//= require ./pretty-text/engines/discourse-markdown/newline
//= require ./pretty-text/engines/discourse-markdown/html_img

View File

@ -3,11 +3,8 @@
//= require ./pretty-text/censored-words
//= require ./pretty-text/emoji/data
//= require ./pretty-text/emoji
//= require ./pretty-text/engines/discourse-markdown
//= require ./pretty-text/engines/discourse-markdown-it
//= require_tree ./pretty-text/engines/discourse-markdown
//= require xss.min
//= require better_markdown.js
//= require ./pretty-text/xss
//= require ./pretty-text/white-lister
//= require ./pretty-text/sanitizer

View File

@ -151,7 +151,7 @@ export function setup(opts, siteSettings, state) {
}
// we got to require this late cause bundle is not loaded in pretty-text
Helpers = Helpers || requirejs('pretty-text/engines/markdown-it/helpers');
Helpers = Helpers || requirejs('pretty-text/engines/discourse-markdown/helpers');
opts.markdownIt = true;

View File

@ -1,27 +0,0 @@
// This addition handles auto linking of text. When included, it will parse out links and create
// `<a href>`s for them.
const urlReplacerArgs = {
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/,
spaceOrTagBoundary: true,
emitter(matches) {
const url = matches[1];
let href = url;
// Don't autolink a markdown link to something
if (url.match(/\]\[\d$/)) { return; }
// If we improperly caught a markdown link abort
if (url.match(/\(http/)) { return; }
if (url.match(/^www/)) { href = "http://" + url; }
return ['a', { href }, url];
}
};
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
helper.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
}

View File

@ -1,4 +1,4 @@
import { parseBBCodeTag } from 'pretty-text/engines/markdown-it/bbcode-block';
import { parseBBCodeTag } from 'pretty-text/engines/discourse-markdown/bbcode-block';
function tokanizeBBCode(state, silent, ruler) {

View File

@ -1,170 +0,0 @@
export function register(helper, codeName, args, emitter) {
// Optional second param for args
if (typeof args === "function") {
emitter = args;
args = {};
}
helper.replaceBlock({
start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"),
emitter(blockContents, matches) {
const options = helper.getOptions();
while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) {
blockContents[0] = String(blockContents[0]).replace(/^\s+/, '');
if (!blockContents[0].length) {
blockContents.shift();
} else {
break;
}
}
let contents = [];
if (blockContents.length) {
const nextContents = blockContents.slice(1);
blockContents = this.processBlock(blockContents[0], nextContents);
nextContents.forEach(nc => {
blockContents = blockContents.concat(this.processBlock(nc, []));
});
blockContents.forEach(bc => {
if (typeof bc === "string" || bc instanceof String) {
var processed = this.processInline(String(bc));
if (processed.length) {
contents.push(['p'].concat(processed));
}
} else {
contents.push(bc);
}
});
}
if (!args.singlePara && contents.length === 1 && contents[0] instanceof Array && contents[0][0] === "para") {
contents[0].shift();
contents = contents[0];
}
const result = emitter(contents, matches[1] ? matches[1].replace(/^=|\"/g, '') : null, options);
return args.noWrap ? result : ['p', result];
}
});
};
export function builders(helper) {
function replaceBBCode(tag, emitter, opts) {
const start = `[${tag}]`;
const stop = `[/${tag}]`;
opts = opts || {};
opts = _.merge(opts, { start, stop, emitter });
helper.inlineBetween(opts);
opts = _.merge(opts, { start: start.toUpperCase(), stop: stop.toUpperCase(), emitter });
helper.inlineBetween(opts);
}
return {
replaceBBCode,
register(codeName, args, emitter) {
register(helper, codeName, args, emitter);
},
rawBBCode(tag, emitter) {
replaceBBCode(tag, emitter, { rawContents: true });
},
removeEmptyLines(contents) {
const result = [];
for (let i=0; i < contents.length; i++) {
if (contents[i] !== "\n") { result.push(contents[i]); }
}
return result;
},
replaceBBCodeParamsRaw(tag, emitter) {
var opts = {
rawContents: true,
emitter(contents) {
const m = /^([^\]]+)\]([\S\s]*)$/.exec(contents);
if (m) { return emitter.call(this, m[1], m[2]); }
}
};
helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
tag = tag.toUpperCase();
helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
}
};
}
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']);
const { replaceBBCode, rawBBCode, removeEmptyLines, replaceBBCodeParamsRaw } = builders(helper);
replaceBBCode('b', contents => ['span', {'class': 'bbcode-b'}].concat(contents));
replaceBBCode('i', contents => ['span', {'class': 'bbcode-i'}].concat(contents));
replaceBBCode('u', contents => ['span', {'class': 'bbcode-u'}].concat(contents));
replaceBBCode('s', contents => ['span', {'class': 'bbcode-s'}].concat(contents));
replaceBBCode('ul', contents => ['ul'].concat(removeEmptyLines(contents)));
replaceBBCode('ol', contents => ['ol'].concat(removeEmptyLines(contents)));
replaceBBCode('li', contents => ['li'].concat(removeEmptyLines(contents)));
rawBBCode('img', href => ['img', {href}]);
rawBBCode('email', contents => ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]);
replaceBBCode('url', contents => {
if (!Array.isArray(contents)) { return; }
const first = contents[0];
if (contents.length === 1 && Array.isArray(first) && first[0] === 'a') {
// single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link.
if (typeof first[1] !== 'object') { first.splice(1, 0, {}); }
first[1]['data-bbcode'] = true;
}
return ['concat'].concat(contents);
});
replaceBBCodeParamsRaw('url', function(param, contents) {
const url = param.replace(/(^")|("$)/g, '');
return ['a', {'href': url}].concat(this.processInline(contents));
});
replaceBBCodeParamsRaw("email", function(param, contents) {
return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents);
});
helper.onParseNode(event => {
if (!Array.isArray(event.node)) { return; }
const result = [event.node[0]];
const nodes = event.node.slice(1);
for (let i = 0; i < nodes.length; i++) {
if (Array.isArray(nodes[i]) && nodes[i][0] === 'concat') {
for (let j = 1; j < nodes[i].length; j++) { result.push(nodes[i][j]); }
} else {
result.push(nodes[i]);
}
}
for (let i = 0; i < result.length; i++) { event.node[i] = result[i]; }
});
helper.replaceBlock({
start: /(\[code\])([\s\S]*)/igm,
stop: /\[\/code\]/igm,
rawContents: true,
emitter(blockContents) {
const options = helper.getOptions();
const inner = blockContents.join("\n");
const defaultCodeLang = options.defaultCodeLang;
return ['p', ['pre', ['code', {'class': `lang-${defaultCodeLang}`}, inner]]];
}
});
}

View File

@ -1,73 +0,0 @@
import guid from 'pretty-text/guid';
/**
markdown-js doesn't ensure that em/strong codes are present on word boundaries.
So we create our own handlers here.
**/
// From PageDown
const aLetter = /[a-zA-Z0-9\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/;
function unhoist(obj,from,to){
let unhoisted = 0;
const regex = new RegExp(from, "g");
if(_.isArray(obj)){
for (let i=0; i<obj.length; i++){
const item = obj[i];
if (_.isString(item)) {
// Odd, but we need +1 for the / in front of /*
const matches = item.match(regex);
unhoisted -= matches ? matches.length : 0;
obj[i] = item.replace(regex, to);
unhoisted += item.length - obj[i].length;
}
if (_.isArray(item)) {
unhoisted += unhoist(item, from, to);
}
}
}
return unhoisted;
};
export function setup(helper) {
if (helper.markdownIt) { return; }
function replaceMarkdown(match, tag) {
const hash = guid();
helper.registerInline(match, function(text, matched, prev) {
if (!text || text.length < match.length + 1) { return; }
let lastText = prev[prev.length-1];
lastText = typeof lastText === "string" && lastText;
lastText = lastText && lastText[lastText.length-1];
if (lastText && (lastText === "/" || lastText.match(aLetter))) { return; }
if (text[match.length].match(/\s/)) { return; }
// hoist out escaped \*
text = text.replace(new RegExp("\\\\\\" + match[0], "g"), hash);
const endText = new RegExp("[^\\s|" + match[0] + "]" + match.replace(/\*/g,"\\*") + "([^" + match[0] + "]|$)");
const finish = text.split("\n")[0].search(endText);
if (finish && finish >= 0) {
const newText = this.processInline(text.substring(match.length, finish+1));
const unhoisted_length = unhoist(newText,hash,match[0]);
const array = typeof tag === "string" ? [tag].concat(newText) : [tag[0], [tag[1]].concat(newText)];
return [(finish + match.length + 1) - unhoisted_length, array];
}
});
}
replaceMarkdown('***', ['strong','em']);
replaceMarkdown('___', ['strong','em']);
replaceMarkdown('**', 'strong');
replaceMarkdown('__', 'strong');
replaceMarkdown('*', 'em');
replaceMarkdown('_', 'em');
};

View File

@ -1,20 +1,104 @@
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.inlineRegexp({
start: '#',
matcher: /^#([\w-:]{1,101})/i,
spaceOrTagBoundary: true,
emitter(matches) {
const options = helper.getOptions();
function addHashtag(buffer, matches, state) {
const options = state.md.options.discourse;
const [hashtag, slug] = matches;
const categoryHashtagLookup = options.categoryHashtagLookup;
const result = categoryHashtagLookup && categoryHashtagLookup(slug);
return result ? ['a', { class: 'hashtag', href: result[0] }, '#', ["span", {}, result[1]]]
: ['span', { class: 'hashtag' }, hashtag];
let token;
if (result) {
token = new state.Token('link_open', 'a', 1);
token.attrs = [['class', 'hashtag'], ['href', result[0]]];
token.block = false;
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = '#';
buffer.push(token);
token = new state.Token('span_open', 'span', 1);
token.block = false;
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = result[1];
buffer.push(token);
buffer.push(new state.Token('span_close', 'span', -1));
buffer.push(new state.Token('link_close', 'a', -1));
} else {
token = new state.Token('span_open', 'span', 1);
token.attrs = [['class', 'hashtag']];
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = hashtag;
buffer.push(token);
token = new state.Token('span_close', 'span', -1);
buffer.push(token);
}
}
const REGEX = /#([\w-:]{1,101})/gi;
function allowedBoundary(content, index, utils) {
let code = content.charCodeAt(index);
return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code)));
}
function applyHashtag(content, state) {
let result = null,
match,
pos = 0;
while (match = REGEX.exec(content)) {
// check boundary
if (match.index > 0) {
if (!allowedBoundary(content, match.index-1, state.md.utils)) {
continue;
}
}
// check forward boundary as well
if (match.index + match[0].length < content.length) {
if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) {
continue;
}
}
if (match.index > pos) {
result = result || [];
let token = new state.Token('text', '', 0);
token.content = content.slice(pos, match.index);
result.push(token);
}
result = result || [];
addHashtag(result, match, state);
pos = match.index + match[0].length;
}
if (result && pos < content.length) {
let token = new state.Token('text', '', 0);
token.content = content.slice(pos);
result.push(token);
}
return result;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md=>{
md.core.ruler.push('category-hashtag', state => md.options.discourse.helpers.textReplace(
state, applyHashtag, true /* skip all links */
));
});
}

View File

@ -1,18 +1,44 @@
import { censor } from 'pretty-text/censored-words';
import { registerOption } from 'pretty-text/pretty-text';
import { censorFn } from 'pretty-text/censored-words';
registerOption((siteSettings, opts) => {
opts.features.censored = true;
function recurse(tokens, apply) {
let i;
for(i=0;i<tokens.length;i++) {
apply(tokens[i]);
if (tokens[i].children) {
recurse(tokens[i].children, apply);
}
}
}
function censorTree(state, censor) {
if (!state.tokens) {
return;
}
recurse(state.tokens, token => {
if (token.content) {
token.content = censor(token.content);
}
});
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => {
opts.censoredWords = siteSettings.censored_words;
opts.censoredPattern = siteSettings.censored_pattern;
});
export function setup(helper) {
helper.registerPlugin(md => {
const words = md.options.discourse.censoredWords;
const patterns = md.options.discourse.censoredPattern;
if (helper.markdownIt) { return; }
helper.addPreProcessor(text => {
const options = helper.getOptions();
return censor(text, options.censoredWords, options.censoredPattern);
if ((words && words.length > 0) || (patterns && patterns.length > 0)) {
const replacement = String.fromCharCode(9632);
const censor = censorFn(words, patterns, replacement);
md.core.ruler.push('censored', state => censorTree(state, censor));
}
});
}

View File

@ -1,28 +1,39 @@
import { escape } from 'pretty-text/sanitizer';
import { registerOption } from 'pretty-text/pretty-text';
// we need a custom renderer for code blocks cause we have a slightly non compliant
// format with special handling for text and so on
// Support for various code blocks
const TEXT_CODE_CLASSES = ["text", "pre", "plain"];
function codeFlattenBlocks(blocks) {
let result = "";
blocks.forEach(function(b) {
result += b;
if (b.trailing) { result += b.trailing; }
});
return result;
function render(tokens, idx, options, env, slf, md) {
let token = tokens[idx],
info = token.info ? md.utils.unescapeAll(token.info) : '',
langName = md.options.discourse.defaultCodeLang,
className,
escapedContent = md.utils.escapeHtml(token.content);
if (info) {
// strip off any additional languages
info = info.split(/\s+/g)[0];
}
registerOption((siteSettings, opts) => {
opts.features.code = true;
const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses;
if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) {
langName = info;
}
className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName;
return `<pre><code class='${className}'>${escapedContent}</code></pre>\n`;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => {
opts.defaultCodeLang = siteSettings.default_code_lang;
opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);
});
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.whiteList({
custom(tag, name, value) {
if (tag === 'code' && name === 'class') {
@ -34,50 +45,7 @@ export function setup(helper) {
}
});
helper.replaceBlock({
start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
stop: /^```$/gm,
withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match
emitter(blockContents, matches) {
const opts = helper.getOptions();
let codeLang = opts.defaultCodeLang;
const acceptableCodeClasses = opts.acceptableCodeClasses;
if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) {
codeLang = matches[1];
}
if (TEXT_CODE_CLASSES.indexOf(matches[1]) !== -1) {
return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]];
} else {
return ['p', ['pre', ['code', {'class': 'lang-' + codeLang}, codeFlattenBlocks(blockContents) ]]];
}
}
});
helper.replaceBlock({
start: /(<pre[^\>]*\>)([\s\S]*)/igm,
stop: /<\/pre>/igm,
rawContents: true,
skipIfTradtionalLinebreaks: true,
emitter(blockContents) {
return ['p', ['pre', codeFlattenBlocks(blockContents)]];
}
});
// Ensure that content in a code block is fully escaped. This way it's not white listed
// and we can use HTML and Javascript examples.
helper.onParseNode(function(event) {
const node = event.node,
path = event.path;
if (node[0] === 'code') {
const regexp = (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") ?
/ +$/g : /^ +| +$/g;
const contents = node[node.length-1];
node[node.length-1] = escape(contents.replace(regexp,''));
}
helper.registerPlugin(md=>{
md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md);
});
}

View File

@ -1,117 +1,246 @@
import { registerOption } from 'pretty-text/pretty-text';
import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji';
import { translations } from 'pretty-text/emoji/data';
let _unicodeReplacements;
let _unicodeRegexp;
export function setUnicodeReplacements(replacements) {
_unicodeReplacements = replacements;
if (replacements) {
// We sort and reverse to match longer emoji sequences first
_unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g");
}
};
const MAX_NAME_LENGTH = 60;
function escapeRegExp(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
let translationTree = null;
// This allows us to efficiently search for aliases
// We build a data structure that allows us to quickly
// search through our N next chars to see if any match
// one of our alias emojis.
//
function buildTranslationTree() {
let tree = [];
let lastNode;
Object.keys(translations).forEach(function(key){
let i;
let node = tree;
for(i=0;i<key.length;i++) {
let code = key.charCodeAt(i);
let j;
let found = false;
for (j=0;j<node.length;j++){
if (node[j][0] === code) {
node = node[j][1];
found = true;
break;
}
}
function checkPrev(prev) {
if (prev && prev.length) {
const lastToken = prev[prev.length-1];
if (lastToken && lastToken.charAt) {
const lastChar = lastToken.charAt(lastToken.length-1);
if (!/\W/.test(lastChar)) return false;
if (!found) {
// token, children, value
let tmp = [code, []];
node.push(tmp);
lastNode = tmp;
node = tmp[1];
}
}
return true;
}
registerOption((siteSettings, opts, state) => {
lastNode[1] = translations[key];
});
return tree;
}
function imageFor(code, opts) {
code = code.toLowerCase();
const url = buildEmojiUrl(code, opts);
if (url) {
const title = `:${code}:`;
const classes = isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji";
return {url, title, classes};
}
}
function getEmojiName(content, pos, state) {
if (content.charCodeAt(pos) !== 58) {
return;
}
if (pos > 0) {
let prev = content.charCodeAt(pos-1);
if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) {
return;
}
}
pos++;
if (content.charCodeAt(pos) === 58) {
return;
}
let length = 0;
while(length < MAX_NAME_LENGTH) {
length++;
if (content.charCodeAt(pos+length) === 58) {
// check for t2-t6
if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) {
length += 3;
}
break;
}
if (pos+length > content.length) {
return;
}
}
if (length === MAX_NAME_LENGTH) {
return;
}
return content.substr(pos, length);
}
// straight forward :smile: to emoji image
function getEmojiTokenByName(name, state) {
let info;
if (info = imageFor(name, state.md.options.discourse)) {
let token = new state.Token('emoji', 'img', 0);
token.attrs = [['src', info.url],
['title', info.title],
['class', info.classes],
['alt', info.title]];
return token;
}
}
function getEmojiTokenByTranslation(content, pos, state) {
translationTree = translationTree || buildTranslationTree();
let currentTree = translationTree;
let i;
let search = true;
let found = false;
let start = pos;
while(search) {
search = false;
let code = content.charCodeAt(pos);
for (i=0;i<currentTree.length;i++) {
if(currentTree[i][0] === code) {
currentTree = currentTree[i][1];
pos++;
search = true;
if (typeof currentTree === "string") {
found = currentTree;
}
break;
}
}
}
if (!found) {
return;
}
// quick boundary check
if (start > 0) {
let leading = content.charAt(start-1);
if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) {
return;
}
}
// check trailing for punct or space
if (pos < content.length) {
let trailing = content.charCodeAt(pos);
if (!state.md.utils.isSpace(trailing)){
return;
}
}
let token = getEmojiTokenByName(found, state);
if (token) {
return { pos, token };
}
}
function applyEmoji(content, state, emojiUnicodeReplacer) {
let i;
let result = null;
let contentToken = null;
let start = 0;
if (emojiUnicodeReplacer) {
content = emojiUnicodeReplacer(content);
}
let endToken = content.length;
for (i=0; i<content.length-1; i++) {
let offset = 0;
let emojiName = getEmojiName(content,i,state);
let token = null;
if (emojiName) {
token = getEmojiTokenByName(emojiName, state);
if (token) {
offset = emojiName.length+2;
}
}
if (!token) {
// handle aliases (note: we can't do this in inline cause ; is not a split point)
//
let info = getEmojiTokenByTranslation(content, i, state);
if (info) {
offset = info.pos - i;
token = info.token;
}
}
if (token) {
result = result || [];
if (i-start>0) {
contentToken = new state.Token('text', '', 0);
contentToken.content = content.slice(start,i);
result.push(contentToken);
}
result.push(token);
endToken = start = i + offset;
}
}
if (endToken < content.length) {
contentToken = new state.Token('text', '', 0);
contentToken.content = content.slice(endToken);
result.push(contentToken);
}
return result;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings, state)=>{
opts.features.emoji = !!siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set || "";
opts.customEmoji = state.customEmoji;
});
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.whiteList('img.emoji');
function imageFor(code) {
code = code.toLowerCase();
const opts = helper.getOptions();
const url = buildEmojiUrl(code, opts);
if (url) {
const title = `:${code}:`;
const classes = isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji";
return ['img', { href: url, title, 'class': classes, alt: title }];
}
}
const translationsWithColon = {};
Object.keys(translations).forEach(t => {
if (t[0] === ':') {
translationsWithColon[t] = translations[t];
} else {
const replacement = translations[t];
helper.inlineReplace(t, (token, match, prev) => {
return checkPrev(prev) ? imageFor(replacement) : token;
});
}
});
const translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(t => `(${escapeRegExp(t)})`).join("|"));
helper.registerInline(':', (text, match, prev) => {
const endPos = text.indexOf(':', 1);
const firstSpace = text.search(/\s/);
if (!checkPrev(prev)) { return; }
// If there is no trailing colon, check our translations that begin with colons
if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) {
translationColonRegexp.lastIndex = 0;
const m = translationColonRegexp.exec(text);
if (m && m[0] && text.indexOf(m[0]) === 0) {
// Check outer edge
const lastChar = text.charAt(m[0].length);
if (lastChar && !/\s/.test(lastChar)) return;
const contents = imageFor(translationsWithColon[m[0]]);
if (contents) {
return [m[0].length, contents];
}
}
return;
}
let between;
const emojiNameMatch = text.match(/(?:.*?)(:(?!:).?[\w-]*(?::t\d)?:)/);
if (emojiNameMatch) {
between = emojiNameMatch[0].slice(1, -1);
} else {
between = text.slice(1, -1);
}
const contents = imageFor(between);
if (contents) {
return [text.indexOf(between, 1) + between.length + 1, contents];
}
});
helper.addPreProcessor(text => {
if (_unicodeReplacements) {
_unicodeRegexp.lastIndex = 0;
let m;
while ((m = _unicodeRegexp.exec(text)) !== null) {
let replacement = ":" + _unicodeReplacements[m[0]] + ":";
const before = text.charAt(m.index-1);
if (!/\B/.test(before)) {
replacement = "\u200b" + replacement;
}
text = text.replace(m[0], replacement);
}
}
return text;
helper.registerPlugin((md)=>{
md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace(
state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer))
);
});
}

View File

@ -1,52 +0,0 @@
const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'details',
'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output',
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video', 'summary'];
function splitAtLast(tag, block, next, first) {
const endTag = `</${tag}>`;
let endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag);
if (endTagIndex !== -1) {
endTagIndex += endTag.length;
const trailing = block.substr(endTagIndex).replace(/^\s+/, '');
if (trailing.length) {
next.unshift(trailing);
}
return [ block.substr(0, endTagIndex) ];
}
};
export function setup(helper) {
if (helper.markdownIt) { return; }
// If a row begins with HTML tags, don't parse it.
helper.registerBlock('html', function(block, next) {
let split, pos;
// Fix manual blockquote paragraphing even though it's not strictly correct
// PERF NOTE: /\S+<blockquote/ is a perf hog for search, try on huge string
if (pos = block.search(/<blockquote/) >= 0) {
if(block.substring(0, pos).search(/\s/) === -1) {
split = splitAtLast('blockquote', block, next, true);
if (split) { return this.processInline(split[0]); }
}
}
const m = /^\s*<\/?([^>]+)\>/.exec(block);
if (m && m[1]) {
const tag = m[1].split(/\s/);
if (tag && tag[0] && BLOCK_TAGS.indexOf(tag[0]) !== -1) {
split = splitAtLast(tag[0], block, next);
if (split) {
if (split.length === 1 && split[0] === block) { return; }
return split;
}
return [ block.toString() ];
}
}
});
}

View File

@ -1,51 +1,88 @@
/**
Supports our custom @mention syntax for calling out a user in a post.
It will add a special class to them, and create a link if the user is found in a
local map.
**/
const regex = /^(\w[\w.-]{0,59})\b/i;
function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) {
let pos = state.pos;
// 64 = @
if (silent || state.src.charCodeAt(pos) !== 64) {
return false;
}
if (pos > 0) {
let prev = state.src.charCodeAt(pos-1);
if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) {
return false;
}
}
// skip if in a link
if (state.tokens) {
let last = state.tokens[state.tokens.length-1];
if (last) {
if (last.type === 'link_open') {
return false;
}
if (last.type === 'html_inline' && last.content.substr(0,2) === "<a") {
return false;
}
}
}
let maxMention = state.src.substr(pos+1, 60);
let matches = maxMention.match(regex);
if (!matches) {
return false;
}
let username = matches[1];
let type = mentionLookup && mentionLookup(username);
let tag = 'a';
let className = 'mention';
let href = null;
if (type === 'user') {
href = getURL('/u/') + username.toLowerCase();
} else if (type === 'group') {
href = getURL('/groups/') + username;
className = 'mention-group';
} else {
tag = 'span';
}
let token = state.push('mention_open', tag, 1);
token.attrs = [['class', className]];
if (href) {
token.attrs.push(['href', href]);
}
token = state.push('text', '', 0);
token.content = '@'+username;
state.push('mention_close', tag, -1);
state.pos = pos + username.length + 1;
return true;
}
export function setup(helper) {
if (helper.markdownIt) { return; }
if (!helper.markdownIt) { return; }
// We have to prune @mentions that are within links.
helper.onParseNode(event => {
const node = event.node,
path = event.path;
if (node[1] && node[1]["class"] === 'mention') {
const parent = path[path.length - 1];
// If the parent is an 'a', remove it
if (parent && parent[0] === 'a') {
const name = node[2];
node.length = 0;
node[0] = "__RAW";
node[1] = name;
}
}
});
helper.inlineRegexp({
start: '@',
// NOTE: since we can't use SiteSettings here (they loads later in process)
// we are being less strict to account for more cases than allowed
matcher: /^@(\w[\w.-]{0,59})\b/i,
wordBoundary: true,
emitter(matches) {
const mention = matches[0].trim();
const name = matches[1];
const opts = helper.getOptions();
const mentionLookup = opts.mentionLookup;
const type = mentionLookup && mentionLookup(name);
if (type === "user") {
return ['a', {'class': 'mention', href: opts.getURL("/u/") + name.toLowerCase()}, mention];
} else if (type === "group") {
return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention];
} else {
return ['span', {'class': 'mention'}, mention];
}
}
helper.registerPlugin(md => {
md.inline.ruler.push('mentions', (state,silent)=> applyMentions(
state,
silent,
md.utils.isWhiteSpace,
md.utils.isPunctChar,
md.options.discourse.mentionLookup,
md.options.discourse.getURL
));
});
}

View File

@ -1,30 +1,53 @@
// Support for the newline behavior in markdown that most expect. Look through all text nodes
// in the tree, replace any new lines with `br`s.
// see: https://github.com/markdown-it/markdown-it/issues/375
//
// we use a custom paragraph rule cause we have to signal when a
// link starts with a space, so we can bypass a onebox
// this is a freedom patch, so careful, may break on updates
function newline(state, silent) {
var token, pmax, max, pos = state.pos;
if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; }
pmax = state.pending.length - 1;
max = state.posMax;
// ' \n' -> hardbreak
// Lookup in pending chars is bad practice! Don't copy to other rules!
// Pending string is stored in concat mode, indexed lookups will cause
// convertion to flat mode.
if (!silent) {
if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) {
if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) {
state.pending = state.pending.replace(/ +$/, '');
token = state.push('hardbreak', 'br', 0);
} else {
state.pending = state.pending.slice(0, -1);
token = state.push('softbreak', 'br', 0);
}
} else {
token = state.push('softbreak', 'br', 0);
}
}
pos++;
// skip heading spaces for next line
while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) {
if (token) {
token.leading_space = true;
}
pos++;
}
state.pos = pos;
return true;
};
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.postProcessText((text, event) => {
const { options, insideCounts } = event;
if (options.traditionalMarkdownLinebreaks || (insideCounts.pre > 0)) { return; }
if (text === "\n") {
// If the tag is just a new line, replace it with a `<br>`
return [['br']];
} else {
// If the text node contains new lines, perhaps with text between them, insert the
// `<br>` tags.
const split = text.split(/\n+/);
if (split.length) {
const replacement = [];
for (var i=0; i<split.length; i++) {
if (split[i].length > 0) { replacement.push(split[i]); }
if (i !== split.length-1) { replacement.push(['br']); }
}
return replacement;
}
}
helper.registerPlugin(md => {
md.inline.ruler.at('newline', newline);
});
}

View File

@ -1,71 +1,89 @@
import { lookupCache } from 'pretty-text/oneboxer';
// Given a node in the document and its parent, determine whether it is on its own line or not.
function isOnOneLine(link, parent) {
if (!parent) { return false; }
const siblings = parent.slice(1);
if ((!siblings) || (siblings.length < 1)) { return false; }
const idx = siblings.indexOf(link);
if (idx === -1) { return false; }
if (idx > 0) {
const prev = siblings[idx-1];
if (prev[0] !== 'br') { return false; }
}
if (idx < siblings.length) {
const next = siblings[idx+1];
if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; }
}
return true;
}
// We only onebox stuff that is on its own line.
export function setup(helper) {
if (helper.markdownIt) { return; }
helper.onParseNode(event => {
const node = event.node,
path = event.path;
// We only care about links
if (node[0] !== 'a') { return; }
const parent = path[path.length - 1];
// We don't onebox bbcode
if (node[1]['data-bbcode']) {
delete node[1]['data-bbcode'];
function applyOnebox(state, silent) {
if (silent || !state.tokens || state.tokens.length < 3) {
return;
}
// We don't onebox mentions
if (node[1]['class'] === 'mention') { return; }
let i;
for(i=1;i<state.tokens.length;i++) {
let token = state.tokens[i];
// Don't onebox links within a list
for (var i=0; i<path.length; i++) {
if (path[i][0] === 'li') { return; }
}
let prev = state.tokens[i-1];
let prevAccepted = prev.type === "paragraph_open" && prev.level === 0;
// If the link has a different label text than the link itself, don't onebox it.
const label = node[node.length-1];
if (label !== node[1]['href']) { return; }
if (token.type === "inline" && prevAccepted) {
let j;
for(j=0;j<token.children.length;j++){
let child = token.children[j];
if (isOnOneLine(node, parent)) {
if (child.type === "link_open" && child.markup === 'linkify' && child.info === 'auto') {
node[1]['class'] = 'onebox';
node[1].target = '_blank';
if (j === 0 && token.leading_space) {
continue;
} else if (j > 0) {
let prevSibling = token.children[j-1];
const contents = lookupCache(node[1].href);
if (contents) {
node[0] = '__RAW';
node[1] = contents;
node.length = 2;
if (prevSibling.tag !== 'br' || prevSibling.leading_space) {
continue;
}
}
// look ahead for soft or hard break
let text = token.children[j+1];
let close = token.children[j+2];
let lookahead = token.children[j+3];
if (lookahead && lookahead.tag !== 'br') {
continue;
}
// check attrs only include a href
let attrs = child["attrs"];
if (!attrs || attrs.length !== 1 || attrs[0][0] !== "href") {
continue;
}
// we already know text matches cause it is an auto link
if (!close || close.type !== "link_close") {
continue;
}
// we already determined earlier that 0 0 was href
let cached = lookupCache(attrs[0][1]);
if (cached) {
// replace link with 2 blank text nodes and inline html for onebox
child.type = 'html_raw';
child.content = cached;
child.inline = true;
text.type = 'html_raw';
text.content = '';
text.inline = true;
close.type = 'html_raw';
close.content = '';
close.inline = true;
} else {
// decorate...
attrs.push(["class", "onebox"]);
}
}
}
}
}
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => {
md.core.ruler.after('linkify', 'onebox', applyOnebox);
});
}

View File

@ -1,83 +0,0 @@
import { register } from 'pretty-text/engines/discourse-markdown/bbcode';
import { registerOption } from 'pretty-text/pretty-text';
import { performEmojiUnescape } from 'pretty-text/emoji';
registerOption((siteSettings, opts) => {
opts.enableEmoji = siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set;
});
export function setup(helper) {
if (helper.markdownIt) { return; }
register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
const params = {'class': 'quote'};
let username = null;
const opts = helper.getOptions();
if (bbParams) {
const paramsSplit = bbParams.split(/\,\s*/);
username = paramsSplit[0];
paramsSplit.forEach(function(p,i) {
if (i > 0) {
var assignment = p.split(':');
if (assignment[0] && assignment[1]) {
const escaped = helper.escape(assignment[0]);
// don't escape attributes, makes no sense
if (escaped === assignment[0]) {
params['data-' + assignment[0]] = helper.escape(assignment[1].trim());
}
}
}
});
}
let avatarImg;
const postNumber = parseInt(params['data-post'], 10);
const topicId = parseInt(params['data-topic'], 10);
if (options.lookupAvatarByPostNumber) {
// client-side, we can retrieve the avatar from the post
avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
} else if (options.lookupAvatar) {
// server-side, we need to lookup the avatar from the username
avatarImg = options.lookupAvatar(username);
}
// If there's no username just return a simple quote
if (!username) {
return ['p', ['aside', params, ['blockquote'].concat(contents)]];
}
const header = ['div', {'class': 'title'},
['div', {'class': 'quote-controls'}],
avatarImg ? ['__RAW', avatarImg] : "",
username ? `${username}:` : "" ];
if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) {
const topicInfo = options.getTopicInfo(topicId);
if (topicInfo) {
var href = topicInfo.href;
if (postNumber > 0) { href += "/" + postNumber; }
// get rid of username said stuff
header.pop();
let title = topicInfo.title;
if (opts.enableEmoji) {
title = performEmojiUnescape(topicInfo.title, {
getURL: opts.getURL, emojiSet: opts.emojiSet
});
}
header.push(['a', {'href': href}, title]);
}
}
return ['aside', params, header, ['blockquote'].concat(contents)];
});
}

View File

@ -1,35 +1,31 @@
import { registerOption } from 'pretty-text/pretty-text';
function tableFlattenBlocks(blocks) {
let result = "";
blocks.forEach(b => {
result += b;
if (b.trailing) { result += b.trailing; }
});
// bypass newline insertion
return result.replace(/[\n\r]/g, " ");
};
registerOption((siteSettings, opts) => {
opts.features.table = !!siteSettings.allow_html_tables;
});
export function setup(helper) {
if (helper.markdownIt) { return; }
if (!helper.markdownIt) { return; }
helper.whiteList(['table', 'table.md-table', 'tbody', 'thead', 'tr', 'th', 'td']);
// this is built in now
// TODO: sanitizer needs fixing, does not properly support this yet
helper.replaceBlock({
start: /(<table[^>]*>)([\S\s]*)/igm,
stop: /<\/table>/igm,
rawContents: true,
priority: 1,
// we need a custom callback for style handling
helper.whiteList({
custom: function(tag,attr,val) {
if (tag !== 'th' && tag !== 'td') {
return false;
}
emitter(contents) {
return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])];
if (attr !== 'style') {
return false;
}
return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center');
}
});
helper.whiteList([
'table',
'tbody',
'thead',
'tr',
'th',
'td',
]);
}

View File

@ -1,104 +0,0 @@
function addHashtag(buffer, matches, state) {
const options = state.md.options.discourse;
const [hashtag, slug] = matches;
const categoryHashtagLookup = options.categoryHashtagLookup;
const result = categoryHashtagLookup && categoryHashtagLookup(slug);
let token;
if (result) {
token = new state.Token('link_open', 'a', 1);
token.attrs = [['class', 'hashtag'], ['href', result[0]]];
token.block = false;
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = '#';
buffer.push(token);
token = new state.Token('span_open', 'span', 1);
token.block = false;
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = result[1];
buffer.push(token);
buffer.push(new state.Token('span_close', 'span', -1));
buffer.push(new state.Token('link_close', 'a', -1));
} else {
token = new state.Token('span_open', 'span', 1);
token.attrs = [['class', 'hashtag']];
buffer.push(token);
token = new state.Token('text', '', 0);
token.content = hashtag;
buffer.push(token);
token = new state.Token('span_close', 'span', -1);
buffer.push(token);
}
}
const REGEX = /#([\w-:]{1,101})/gi;
function allowedBoundary(content, index, utils) {
let code = content.charCodeAt(index);
return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code)));
}
function applyHashtag(content, state) {
let result = null,
match,
pos = 0;
while (match = REGEX.exec(content)) {
// check boundary
if (match.index > 0) {
if (!allowedBoundary(content, match.index-1, state.md.utils)) {
continue;
}
}
// check forward boundary as well
if (match.index + match[0].length < content.length) {
if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) {
continue;
}
}
if (match.index > pos) {
result = result || [];
let token = new state.Token('text', '', 0);
token.content = content.slice(pos, match.index);
result.push(token);
}
result = result || [];
addHashtag(result, match, state);
pos = match.index + match[0].length;
}
if (result && pos < content.length) {
let token = new state.Token('text', '', 0);
token.content = content.slice(pos);
result.push(token);
}
return result;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md=>{
md.core.ruler.push('category-hashtag', state => md.options.discourse.helpers.textReplace(
state, applyHashtag, true /* skip all links */
));
});
}

View File

@ -1,44 +0,0 @@
import { censorFn } from 'pretty-text/censored-words';
function recurse(tokens, apply) {
let i;
for(i=0;i<tokens.length;i++) {
apply(tokens[i]);
if (tokens[i].children) {
recurse(tokens[i].children, apply);
}
}
}
function censorTree(state, censor) {
if (!state.tokens) {
return;
}
recurse(state.tokens, token => {
if (token.content) {
token.content = censor(token.content);
}
});
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => {
opts.censoredWords = siteSettings.censored_words;
opts.censoredPattern = siteSettings.censored_pattern;
});
helper.registerPlugin(md => {
const words = md.options.discourse.censoredWords;
const patterns = md.options.discourse.censoredPattern;
if ((words && words.length > 0) || (patterns && patterns.length > 0)) {
const replacement = String.fromCharCode(9632);
const censor = censorFn(words, patterns, replacement);
md.core.ruler.push('censored', state => censorTree(state, censor));
}
});
}

View File

@ -1,51 +0,0 @@
// we need a custom renderer for code blocks cause we have a slightly non compliant
// format with special handling for text and so on
const TEXT_CODE_CLASSES = ["text", "pre", "plain"];
function render(tokens, idx, options, env, slf, md) {
let token = tokens[idx],
info = token.info ? md.utils.unescapeAll(token.info) : '',
langName = md.options.discourse.defaultCodeLang,
className,
escapedContent = md.utils.escapeHtml(token.content);
if (info) {
// strip off any additional languages
info = info.split(/\s+/g)[0];
}
const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses;
if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) {
langName = info;
}
className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName;
return `<pre><code class='${className}'>${escapedContent}</code></pre>\n`;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => {
opts.defaultCodeLang = siteSettings.default_code_lang;
opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);
});
helper.whiteList({
custom(tag, name, value) {
if (tag === 'code' && name === 'class') {
const m = /^lang\-(.+)$/.exec(value);
if (m) {
return helper.getOptions().acceptableCodeClasses.indexOf(m[1]) !== -1;
}
}
}
});
helper.registerPlugin(md=>{
md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md);
});
}

View File

@ -1,246 +0,0 @@
import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji';
import { translations } from 'pretty-text/emoji/data';
const MAX_NAME_LENGTH = 60;
let translationTree = null;
// This allows us to efficiently search for aliases
// We build a data structure that allows us to quickly
// search through our N next chars to see if any match
// one of our alias emojis.
//
function buildTranslationTree() {
let tree = [];
let lastNode;
Object.keys(translations).forEach(function(key){
let i;
let node = tree;
for(i=0;i<key.length;i++) {
let code = key.charCodeAt(i);
let j;
let found = false;
for (j=0;j<node.length;j++){
if (node[j][0] === code) {
node = node[j][1];
found = true;
break;
}
}
if (!found) {
// token, children, value
let tmp = [code, []];
node.push(tmp);
lastNode = tmp;
node = tmp[1];
}
}
lastNode[1] = translations[key];
});
return tree;
}
function imageFor(code, opts) {
code = code.toLowerCase();
const url = buildEmojiUrl(code, opts);
if (url) {
const title = `:${code}:`;
const classes = isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji";
return {url, title, classes};
}
}
function getEmojiName(content, pos, state) {
if (content.charCodeAt(pos) !== 58) {
return;
}
if (pos > 0) {
let prev = content.charCodeAt(pos-1);
if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) {
return;
}
}
pos++;
if (content.charCodeAt(pos) === 58) {
return;
}
let length = 0;
while(length < MAX_NAME_LENGTH) {
length++;
if (content.charCodeAt(pos+length) === 58) {
// check for t2-t6
if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) {
length += 3;
}
break;
}
if (pos+length > content.length) {
return;
}
}
if (length === MAX_NAME_LENGTH) {
return;
}
return content.substr(pos, length);
}
// straight forward :smile: to emoji image
function getEmojiTokenByName(name, state) {
let info;
if (info = imageFor(name, state.md.options.discourse)) {
let token = new state.Token('emoji', 'img', 0);
token.attrs = [['src', info.url],
['title', info.title],
['class', info.classes],
['alt', info.title]];
return token;
}
}
function getEmojiTokenByTranslation(content, pos, state) {
translationTree = translationTree || buildTranslationTree();
let currentTree = translationTree;
let i;
let search = true;
let found = false;
let start = pos;
while(search) {
search = false;
let code = content.charCodeAt(pos);
for (i=0;i<currentTree.length;i++) {
if(currentTree[i][0] === code) {
currentTree = currentTree[i][1];
pos++;
search = true;
if (typeof currentTree === "string") {
found = currentTree;
}
break;
}
}
}
if (!found) {
return;
}
// quick boundary check
if (start > 0) {
let leading = content.charAt(start-1);
if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) {
return;
}
}
// check trailing for punct or space
if (pos < content.length) {
let trailing = content.charCodeAt(pos);
if (!state.md.utils.isSpace(trailing)){
return;
}
}
let token = getEmojiTokenByName(found, state);
if (token) {
return { pos, token };
}
}
function applyEmoji(content, state, emojiUnicodeReplacer) {
let i;
let result = null;
let contentToken = null;
let start = 0;
if (emojiUnicodeReplacer) {
content = emojiUnicodeReplacer(content);
}
let endToken = content.length;
for (i=0; i<content.length-1; i++) {
let offset = 0;
let emojiName = getEmojiName(content,i,state);
let token = null;
if (emojiName) {
token = getEmojiTokenByName(emojiName, state);
if (token) {
offset = emojiName.length+2;
}
}
if (!token) {
// handle aliases (note: we can't do this in inline cause ; is not a split point)
//
let info = getEmojiTokenByTranslation(content, i, state);
if (info) {
offset = info.pos - i;
token = info.token;
}
}
if (token) {
result = result || [];
if (i-start>0) {
contentToken = new state.Token('text', '', 0);
contentToken.content = content.slice(start,i);
result.push(contentToken);
}
result.push(token);
endToken = start = i + offset;
}
}
if (endToken < content.length) {
contentToken = new state.Token('text', '', 0);
contentToken.content = content.slice(endToken);
result.push(contentToken);
}
return result;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings, state)=>{
opts.features.emoji = !!siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set || "";
opts.customEmoji = state.customEmoji;
});
helper.registerPlugin((md)=>{
md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace(
state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer))
);
});
}

View File

@ -1,88 +0,0 @@
const regex = /^(\w[\w.-]{0,59})\b/i;
function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) {
let pos = state.pos;
// 64 = @
if (silent || state.src.charCodeAt(pos) !== 64) {
return false;
}
if (pos > 0) {
let prev = state.src.charCodeAt(pos-1);
if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) {
return false;
}
}
// skip if in a link
if (state.tokens) {
let last = state.tokens[state.tokens.length-1];
if (last) {
if (last.type === 'link_open') {
return false;
}
if (last.type === 'html_inline' && last.content.substr(0,2) === "<a") {
return false;
}
}
}
let maxMention = state.src.substr(pos+1, 60);
let matches = maxMention.match(regex);
if (!matches) {
return false;
}
let username = matches[1];
let type = mentionLookup && mentionLookup(username);
let tag = 'a';
let className = 'mention';
let href = null;
if (type === 'user') {
href = getURL('/u/') + username.toLowerCase();
} else if (type === 'group') {
href = getURL('/groups/') + username;
className = 'mention-group';
} else {
tag = 'span';
}
let token = state.push('mention_open', tag, 1);
token.attrs = [['class', className]];
if (href) {
token.attrs.push(['href', href]);
}
token = state.push('text', '', 0);
token.content = '@'+username;
state.push('mention_close', tag, -1);
state.pos = pos + username.length + 1;
return true;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => {
md.inline.ruler.push('mentions', (state,silent)=> applyMentions(
state,
silent,
md.utils.isWhiteSpace,
md.utils.isPunctChar,
md.options.discourse.mentionLookup,
md.options.discourse.getURL
));
});
}

View File

@ -1,53 +0,0 @@
// see: https://github.com/markdown-it/markdown-it/issues/375
//
// we use a custom paragraph rule cause we have to signal when a
// link starts with a space, so we can bypass a onebox
// this is a freedom patch, so careful, may break on updates
function newline(state, silent) {
var token, pmax, max, pos = state.pos;
if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; }
pmax = state.pending.length - 1;
max = state.posMax;
// ' \n' -> hardbreak
// Lookup in pending chars is bad practice! Don't copy to other rules!
// Pending string is stored in concat mode, indexed lookups will cause
// convertion to flat mode.
if (!silent) {
if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) {
if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) {
state.pending = state.pending.replace(/ +$/, '');
token = state.push('hardbreak', 'br', 0);
} else {
state.pending = state.pending.slice(0, -1);
token = state.push('softbreak', 'br', 0);
}
} else {
token = state.push('softbreak', 'br', 0);
}
}
pos++;
// skip heading spaces for next line
while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) {
if (token) {
token.leading_space = true;
}
pos++;
}
state.pos = pos;
return true;
};
export function setup(helper) {
helper.registerPlugin(md => {
md.inline.ruler.at('newline', newline);
});
}

View File

@ -1,89 +0,0 @@
import { lookupCache } from 'pretty-text/oneboxer';
function applyOnebox(state, silent) {
if (silent || !state.tokens || state.tokens.length < 3) {
return;
}
let i;
for(i=1;i<state.tokens.length;i++) {
let token = state.tokens[i];
let prev = state.tokens[i-1];
let prevAccepted = prev.type === "paragraph_open" && prev.level === 0;
if (token.type === "inline" && prevAccepted) {
let j;
for(j=0;j<token.children.length;j++){
let child = token.children[j];
if (child.type === "link_open" && child.markup === 'linkify' && child.info === 'auto') {
if (j === 0 && token.leading_space) {
continue;
} else if (j > 0) {
let prevSibling = token.children[j-1];
if (prevSibling.tag !== 'br' || prevSibling.leading_space) {
continue;
}
}
// look ahead for soft or hard break
let text = token.children[j+1];
let close = token.children[j+2];
let lookahead = token.children[j+3];
if (lookahead && lookahead.tag !== 'br') {
continue;
}
// check attrs only include a href
let attrs = child["attrs"];
if (!attrs || attrs.length !== 1 || attrs[0][0] !== "href") {
continue;
}
// we already know text matches cause it is an auto link
if (!close || close.type !== "link_close") {
continue;
}
// we already determined earlier that 0 0 was href
let cached = lookupCache(attrs[0][1]);
if (cached) {
// replace link with 2 blank text nodes and inline html for onebox
child.type = 'html_raw';
child.content = cached;
child.inline = true;
text.type = 'html_raw';
text.content = '';
text.inline = true;
close.type = 'html_raw';
close.content = '';
close.inline = true;
} else {
// decorate...
attrs.push(["class", "onebox"]);
}
}
}
}
}
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => {
md.core.ruler.after('linkify', 'onebox', applyOnebox);
});
}

View File

@ -1,31 +0,0 @@
export function setup(helper) {
if (!helper.markdownIt) { return; }
// this is built in now
// TODO: sanitizer needs fixing, does not properly support this yet
// we need a custom callback for style handling
helper.whiteList({
custom: function(tag,attr,val) {
if (tag !== 'th' && tag !== 'td') {
return false;
}
if (attr !== 'style') {
return false;
}
return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center');
}
});
helper.whiteList([
'table',
'tbody',
'thead',
'tr',
'th',
'td',
]);
}

View File

@ -1,4 +1,3 @@
import { cook, setup } from 'pretty-text/engines/discourse-markdown';
import { cook as cookIt, setup as setupIt } from 'pretty-text/engines/discourse-markdown-it';
import { sanitize } from 'pretty-text/sanitizer';
import WhiteLister from 'pretty-text/white-lister';
@ -25,10 +24,6 @@ export function buildOptions(state) {
emojiUnicodeReplacer
} = state;
if (!siteSettings.enable_experimental_markdown_it) {
setup();
}
const features = {
'bold-italics': true,
'auto-link': true,
@ -56,15 +51,10 @@ export function buildOptions(state) {
mentionLookup: state.mentionLookup,
emojiUnicodeReplacer,
allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null,
markdownIt: siteSettings.enable_experimental_markdown_it
markdownIt: true
};
if (siteSettings.enable_experimental_markdown_it) {
setupIt(options, siteSettings, state);
} else {
// TODO deprecate this
_registerFns.forEach(fn => fn(siteSettings, options, state));
}
return options;
}
@ -74,22 +64,13 @@ export default class {
this.opts = opts || {};
this.opts.features = this.opts.features || {};
this.opts.sanitizer = (!!this.opts.sanitize) ? (this.opts.sanitizer || sanitize) : identity;
// We used to do a failsafe call to setup here
// under new engine we always expect setup to be called by buildOptions.
// setup();
}
cook(raw) {
if (!raw || raw.length === 0) { return ""; }
let result;
if (this.opts.markdownIt) {
result = cookIt(raw, this.opts);
} else {
result = cook(raw, this.opts);
}
return result ? result : "";
}

View File

@ -42,9 +42,7 @@
Discourse.Environment = '<%= Rails.env %>';
Discourse.SiteSettings = ps.get('siteSettings');
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
<%- if SiteSetting.enable_experimental_markdown_it %>
Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>';
<%- end %>
I18n.defaultLocale = '<%= SiteSetting.default_locale %>';
Discourse.start();
Discourse.set('assetVersion','<%= Discourse.assets_digest %>');

View File

@ -946,7 +946,6 @@ ar:
notify_mods_when_user_blocked: "إذا تم حظر المستخدم تلقائيا، وإرسال رسالة الى جميع المشرفين."
flag_sockpuppets: "إذا رد أحد المستخدمين جديد إلى موضوع من عنوان IP نفسه باسم المستخدم الجديد الذي بدأ هذا الموضوع، علم كل من مناصبهم كدعاية المحتملين."
traditional_markdown_linebreaks: "استعمل السطور التالفه التقليديه في Markdown, التي تتطلب مساحتين بيضاوين للسطور التالفه"
allow_html_tables: "كل الجداول يجب ان تدخل ب لغة ال HTML مثال TABLE , THEAD , TD , TR , TH سوف يأوذن لهم ( تتطلب مراجعة لكل المقالات القديمة )"
post_undo_action_window_mins: "عدد الدقائق التي يسمح فيها للأعضاء بالتراجع عن آخر إجراءاتهم على المنشور (إعجاب، اشارة، إلخ...)"
must_approve_users: "يجب أن الموظفين يوافق على جميع حسابات المستخدم الجديدة قبل أن يتم السماح لهم للوصول إلى الموقع. تحذير: تمكين هذا لموقع الحية إلغاء وصول المستخدمين الحاليين غير الموظفين!"
pending_users_reminder_delay: "نبه المشرفين إذا وجد اعضاء ينتظرون الموافقة لمدة اطول من الساعات ، قم بوضع الخيار -1 لايقاف التنبيهات ."

View File

@ -903,7 +903,6 @@ da:
notify_mods_when_user_blocked: "Send en besked til alle moderatorer hvis en bruger blokeres automatisk"
flag_sockpuppets: "Hvis en ny bruger svarer på et emne fra den samme IP adresse som den der startede emnet, så rapporter begge at deres indlæg potentielt er spam."
traditional_markdown_linebreaks: "Brug traditionelle linjeskift i Markdown, som kræver 2 mellemrum i slutningen af sætningen."
allow_html_tables: "Tillad tabeller at blive oprettet i Markdown med brug af HTML tags. TABLE, THEAD, TD, TR, TH vil blive tilladt (kræver en fuld re-indeksering af gamle indlæg som benytter tabeller) "
post_undo_action_window_mins: "Antal minutter som brugere er tilladt at fortryde handlinger på et indlæg (like, flag, etc)."
must_approve_users: "Personale skal godkende alle nye bruger konti inden de kan tilgå sitet. ADVARSEL: aktivering af dette for et live site vil medføre en ophævning af adgang for eksisterende ikke-personale brugere."
pending_users_reminder_delay: "Underret moderatorer hvis nye brugere har ventet på godkendelse i længere end så mange timer. Skriv -1 for at deaktivere notifikationer."

View File

@ -921,7 +921,6 @@ de:
notify_mods_when_user_blocked: "Wenn ein Benutzer automatisch gesperrt wird, sende eine Nachricht an alle Moderatoren."
flag_sockpuppets: "Wenn ein neuer Benutzer auf ein Thema antwortet, das von einem anderen neuen Benutzer aber mit der gleichen IP-Adresse begonnen wurde, markiere beide Beiträge als potenziellen Spam."
traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, die zwei nachfolgende Leerzeichen für einen Zeilenumbruch benötigen."
allow_html_tables: "Erlaube es, Tabellen in Markdown mit HTML-Tags einzugeben. TABLE, THEAD, TD, TR, TH werden erlaubt (alle Beiträge mit Tabellen müssen ihr HTML erneuern)"
post_undo_action_window_mins: "Minuten, die ein Benutzer hat, um Aktionen auf einen Beitrag rückgängig zu machen (Gefällt mir, Meldung, usw.)."
must_approve_users: "Team-Mitglieder müssen alle neuen Benutzerkonten freischalten, bevor diese Zugriff auf die Website erhalten. ACHTUNG: Das Aktivieren dieser Option für eine Live-Site entfernt den Zugriff auch für alle existierenden Benutzer außer für Team-Mitglieder!"
pending_users_reminder_delay: "Benachrichtige die Moderatoren, falls neue Benutzer mehr als so viele Stunden auf ihre Genehmigung gewartet haben. Stelle -1 ein, um diese Benachrichtigungen zu deaktivieren."

View File

@ -890,7 +890,6 @@ el:
notify_mods_when_user_blocked: "Εάν ένας χρήστης αυτόματα μπλοκαριστει, στείλε μήνυμα σε όλους τους συντονιστές."
flag_sockpuppets: "Εάν ένας νέος χρήστης απαντήσει σε ένα νήμα από την ίδια διεύθυνση ΙP όπως ο νέος χρήστης, ο οποίος ξεκίνησε το νήμα, και οι δυο δημοσιεύσεις τους θα επισημανθούν ως δυνητικά ανεπιθύμητες."
traditional_markdown_linebreaks: "Χρήση παραδοσιακών αλλαγών γραμμών στη Markdown, η οποία απαιτεί δύο κενά διαστήματα για μια αλλαγή γραμμής."
allow_html_tables: "Αποδοχή εισδοχής πινάκων στη Markdown με τη χρήση ετικετών HTML. TABLE, THEAD, TD, TR, TH θα μπαίνουν στη λίστα επιτρεπόμενων (απαιτείται πληρής αντιγραφή σε όλες τις αναρτήσεις που περιέχουν πίνακες)"
post_undo_action_window_mins: "Αριθμός των λεπτών όπου οι χρήστες δικαιούνται να αναιρέσουν πρόσφατες ενέργειες πάνω σε ένα θέμα (μου αρέσει, επισήμανση, κτλ) "
must_approve_users: "Το προσωπικό πρέπει να εγκρίνει όλους τους λογαριασμούς των νέων χρηστών προτού τους επιτραπεί να έχουν πρόσβαση στην ιστοσελίδα. Προειδοποίηση: ενεργοποιώντας το για μια ζωντανή ιστοσελίδα θα έχει ως αποτέλεσμα την ανάκληση για τους υπάρχοντες χρήστες που δεν ανήκουν στο προσωπικό!"
pending_users_reminder_delay: "Ειδοποίηση συντονιστών αν καινούργιοι χρήστες περιμένουν για αποδοχή για μεγαλύτερο απο αυτό το χρονικό διάστημα. Όρισέ το στο -1 για να απενεργοποιηθούν οι ειδοποιήσεις."

View File

@ -1024,9 +1024,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."
enable_experimental_markdown_it: "Enable the experimental markdown.it CommonMark engine, WARNING: some plugins may not work correctly"
enable_markdown_typographer: "Use basic typography rules to improve text readability of paragraphs of text, replaces (c) (tm) etc, with symbols, reduces number of question marks and so on"
allow_html_tables: "Allow tables to be entered in Markdown using HTML tags. TABLE, THEAD, TD, TR, TH will be whitelisted (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!"
pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications."

View File

@ -932,9 +932,7 @@ es:
notify_mods_when_user_blocked: "Si un usuario es bloqueado automáticamente, enviar un mensaje a todos los moderadores."
flag_sockpuppets: "Si un nuevo usuario responde a un tema desde la misma dirección de IP que el nuevo usuario que inició el tema, reportar los posts de los dos como spam en potencia."
traditional_markdown_linebreaks: "Utiliza saltos de línea tradicionales en Markdown, que requieren dos espacios al final para un salto de línea."
enable_experimental_markdown_it: "Habilitar el motor experimental CommonMark markdown.it, ADVERTENCIA: algunos plugins podrían no funcionar correctamente."
enable_markdown_typographer: "Utilice reglas básicas de tipografía para mejorar la legibilidad de texto de los párrafos de texto, reemplaza (c) (tm) etc, con símbolos, reduce el número de signos de interrogación y así sucesivamente"
allow_html_tables: "Permitir la inserción de tablas en Markdown usando etiquetas HTML. Se permitirá usar TABLE, THEAD, TD, TR o TH (requiere un rebake completo para los posts antiguos que contengan tablas)"
post_undo_action_window_mins: "Número de minutos durante los cuales los usuarios pueden deshacer sus acciones recientes en un post (me gusta, reportes, etc)."
must_approve_users: "Los miembros administración deben aprobar todas las nuevas cuentas antes de que se les permita el acceso al sitio. AVISO: ¡habilitar esta opción en un sitio activo revocará el acceso a los usuarios que no sean moderadores o admin!"
pending_users_reminder_delay: "Notificar a los moderadores si hay nuevos usuarios que hayan estado esperando aprbación durante más estas horas. Usa -1 para desactivar estas notificaciones."

View File

@ -884,7 +884,6 @@ fa_IR:
notify_mods_when_user_blocked: "اگر کاربر به‌طور خودکار مسدود شد، به تمام مدیران پیام بفرست."
flag_sockpuppets: "اگر کاربری به موضوع با ای پی برابر با کاربری که نوشته را شروع کرده ٬ آنها را به عنوان هرزنامه پرچم گزاری کن."
traditional_markdown_linebreaks: "در مدل‌های نشانه گزاری از خط جدید سنتی استفاده کن،‌ که برای linebreak نیاز به دو فضای انتهایی دارد ."
allow_html_tables: "اجازه ارسال جدول به صورت markdown با تگ های HTML. TABLE, THEAD, TD, TR, TH قابل استفاده هستند. (نیازمند ایجا دوباره در نوشته‌های قدیمی که شامل جدول هستند)"
post_undo_action_window_mins: "تعداد دقایقی که کاربران اجازه دارند اقدامی را که در نوشته انجام داده اند باز گردانند. (پسند، پرچم گذاری،‌ چیزهای دیگر)."
must_approve_users: "همکاران باید تمامی حساب‌های کاربری را قبل از اجازه دسترسی به سایت تایید کنند. اخطار: فعال‌سازی این گزینه ممکن است باعث جلوگیری از دسترسی کاربرانی که قبلا عضو شده‌اند نیز بشود!"
pending_users_reminder_delay: "اگر کاربر‌ها بیشتر از این مقدار ساعت منتظر تایید بودند به مدیران اعلام کن. مقدار -1 برای غیرفعال‌سازی."

View File

@ -931,9 +931,7 @@ fi:
notify_mods_when_user_blocked: "Jos käyttäjä estetään automaattisesti, lähetä viesti kaikille valvojille."
flag_sockpuppets: "Jos uuden käyttäjän luomaan ketjuun vastaa toinen uusi käyttäjä samasta IP-osoitteesta, liputa molemmat viestit mahdolliseksi roskapostiksi."
traditional_markdown_linebreaks: "Käytä perinteisiä rivinvaihtoja Markdownissa, joka vaatii kaksi perättäistä välilyöntiä rivin vaihtoon."
enable_experimental_markdown_it: "Ota käyttöön kokeellinen markdown.it Commonmark ohjelmistomoottori. VAROITUS: jotkut lisäosat voivat lakata toimimasta oikein"
enable_markdown_typographer: "Käytetään tavanomaisia typografisia sääntöjä parantamaan tekstikappaleiden luettavuutta, (c), (tm) ym. korvataan symboleilla, kysymysmerkkien määrää vähennetään jne."
allow_html_tables: "Salli taulukoiden syöttäminen Markdowniin käyttäen HTML tageja. TABLE, THEAD, TD, TR, TH valkolistataan (edellyttää kaikkien taulukoita sisältävien vanhojen viestien uudelleen rakentamisen)"
post_undo_action_window_mins: "Kuinka monta minuuttia käyttäjällä on aikaa perua viestiin kohdistuva toimi (tykkäys, liputus, etc)."
must_approve_users: "Henkilökunnan täytyy hyväksyä kaikki uudet tilit, ennen uusien käyttäjien päästämistä sivustolle. VAROITUS: tämän asetuksen valitseminen poistaa pääsyn kaikilta jo olemassa olevilta henkilökuntaan kuulumattomilta käyttäjiltä."
pending_users_reminder_delay: "Ilmoita valvojille, jos uusi käyttäjä on odottanut hyväksyntää kauemmin kuin näin monta tuntia. Aseta -1, jos haluat kytkeä ilmoitukset pois päältä."

View File

@ -931,7 +931,6 @@ fr:
notify_mods_when_user_blocked: "Si un utilisateur est bloqué automatiquement, envoyer un message à tous les modérateurs."
flag_sockpuppets: "Si un nouvel utilisateur répond à un sujet avec la même adresse IP que le nouvel utilisateur qui a commencé le sujet, alors leurs messages seront automatiquement marqués comme spam."
traditional_markdown_linebreaks: "Utiliser le retour à la ligne traditionnel dans Markdown, qui nécessite deux espaces pour un saut de ligne."
allow_html_tables: "Autoriser la saisie des tableaux dans le Markdown en utilisant les tags HTML : TABLE, THEAD, TD, TR, TH sont autorisés (nécessite un rebake de tous les anciens messages contenant des tableaux)"
post_undo_action_window_mins: "Nombre de minutes pendant lesquelles un utilisateur peut annuler une action sur un message (J'aime, signaler, etc.)"
must_approve_users: "Les responsables doivent approuver les nouveaux utilisateurs afin qu'ils puissent accéder au site. ATTENTION : activer cette option sur un site en production suspendra l'accès des utilisateurs existants qui ne sont pas des responsables !"
pending_users_reminder_delay: "Avertir les modérateurs si des nouveaux utilisateurs sont en attente d'approbation depuis x heures. Mettre -1 pour désactiver les notifications."

View File

@ -921,7 +921,6 @@ he:
notify_mods_when_user_blocked: "אם משתמש נחסם אוטומטית, שילחו הודעה לכל המנחים."
flag_sockpuppets: "אם משתמשים חדשים מגיבים לנושא מכתובת IP זהה לזו של מי שהחל את הנושא, סמנו את הפוסטים של שניהם כספאם פוטנציאלי."
traditional_markdown_linebreaks: "שימוש בשבירת שורות מסורתית בסימון, מה שדורש שני רווחים עוקבים למעבר שורה."
allow_html_tables: "אפשרו הכנסת טבלאות ב Markdown באמצעות תגיות HTML. התגיות TABLE, THEAD, TD, TR, TH יהיו ברשימה לבנה (מצריך אפייה מחדש של כל הפוסטים הישנים שכוללים טבלאות)"
post_undo_action_window_mins: "מספר הדקות בהן מתאפשר למשתמשים לבטל פעולות אחרות בפוסט (לייק, סימון, וכו')."
must_approve_users: "על הצוות לאשר את כל המשתמשים החדשים לפני שהם מקבלים גישה לאתר. אזהרה: בחירה זו עבור אתר קיים תשלול גישה ממשתמשים קיימים שאינם מנהלים."
pending_users_reminder_delay: "הודיעו למנחים אם משתמשים חדשים ממתינים לאישור למעלה מכמות זו של שעות. קבעו ל -1 כדי לנטרל התראות."

View File

@ -934,7 +934,6 @@ it:
notify_mods_when_user_blocked: "Se un utente è bloccato automaticamente, manda un messaggio ai moderatori."
flag_sockpuppets: "Se un nuovo utente risponde ad un argomento dallo stesso indirizzo IP dell'utente che ha aperto l'argomento stesso, segnala entrambi i messaggi come potenziale spam."
traditional_markdown_linebreaks: "Usa l'accapo tradizionale in Markdown, cioè due spazi a fine riga per andare a capo."
allow_html_tables: "Consenti di inserire tabelle in Markdown usando tag HTML. I tag TABLE, THEAD, TD, TR, TH saranno consentiti (richiede un full rebake di tutti i vecchi messaggi contenenti tabelle)"
post_undo_action_window_mins: "Numero di minuti durante i quali gli utenti possono annullare le loro azioni recenti su un messaggio (segnalazioni, Mi piace, ecc.)."
must_approve_users: "Lo staff deve approvare tutti i nuovi account utente prima che essi possano accedere al sito. ATTENZIONE: abilitare l'opzione per un sito live revocherà l'accesso per tutti gli utenti non-staff esistenti!"
pending_users_reminder_delay: "Notifica i moderatori se nuovi utenti sono in attesa di approvazione per più di queste ore. Imposta a -1 per disabilitare le notifiche."

View File

@ -876,9 +876,7 @@ ko:
notify_mods_when_user_blocked: "만약 사용자가 자동 블락되면 중간 운영자에게 메시지 보내기"
flag_sockpuppets: "어떤 신규 사용자(예:24이내 가입자)가 글타래를 생성하고 같은 IP주소의 또 다른 신규 사용자가 댓글을 쓰면 자동 스팸 신고"
traditional_markdown_linebreaks: "Markdown에서 전통적인 linebreak를 사용, linebreak시 두개의 trailing space를 사용하는 것."
enable_experimental_markdown_it: "(실험) CommonMark를 지원하는 markdown.it 엔진을 사용합니다. 경고: 올바르게 작동하지 않는 플러그인이 있을 수 있습니다."
enable_markdown_typographer: "문단의 가독성을 높이기 위해서 기본 타이포그라피 룰을 사용합니다. (c) (tm), 기타 기호를 교체하고 연달아 나오는 물음표의 갯수를 줄입니다."
allow_html_tables: "마크다운 문서에 HTML 테이블을 허용합니다. TABLE, THEAD, TD, TR, TH 태그를 사용할 수 있습니다.(테이블이 포함된 이전 게시물에 적용하려면 rebake 해야 합니다.)"
post_undo_action_window_mins: "사용자가 어떤 글에 대해서 수행한 작업(신고 등)을 취소하는 것이 허용되는 시간(초)"
must_approve_users: "스태프는 반드시 사이트 엑세스권한을 허용하기 전에 모든 신규가입계정을 승인해야 합니다. 경고: 이것을 활성화하면 기존 스태프 아닌 회원들의 엑세스권한이 회수됩니다."
pending_users_reminder_delay: "새로운 사용자가 승인을 기다리는 시간이 여기에 지정된 시간횟수보다 더 길어길경우 운영자에게 알려줍니다. 알림을 해제하려면 -1로 설정하세요."

View File

@ -895,7 +895,6 @@ nl:
notify_mods_when_user_blocked: "Als een gebruiker automatisch geblokkeerd is, stuur dan een bericht naar alle moderatoren."
flag_sockpuppets: "Als een nieuwe gebruiker antwoord op een topic vanaf hetzelfde ip-adres als de nieuwe gebruiker die het topic opende, markeer dan beide berichten als potentiële spam."
traditional_markdown_linebreaks: "Gebruik traditionele regeleinden in Markdown, welke 2 spaties aan het einde van een regel nodig heeft voor een regeleinde."
allow_html_tables: "Sta toe dat tabellen in Markdown mogen worden ingevoerd met behulp van HTML-tags. TABLE, TD, TR, TH zullen aan de whitelist worden toegevoegd (vereist volledig herbouwen van alle oude berichten met tabellen)"
post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)."
must_approve_users: "Stafleden moeten alle nieuwe gebruikersaccounts goedkeuren voordat ze de site mogen bezoeken. OPGELET: als dit wordt aangezet voor een actieve site wordt alle toegang voor bestaande niet stafleden ingetrokken."
pending_users_reminder_delay: "Moderators informeren als nieuwe gebruikers al langer dan dit aantal uren op goedkeuring wachten. Stel dit in op -1 om meldingen uit te schakelen."

View File

@ -956,7 +956,6 @@ pl_PL:
notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators."
flag_sockpuppets: "Jeśli nowy użytkownik odpowiada na dany temat z tego samego adresu IP co nowy użytkownik, który założył temat, oznacz ich posty jako potencjalny spam."
traditional_markdown_linebreaks: "Używaj tradycyjnych znaków końca linii w Markdown, to znaczy dwóch spacji na końcu linii."
allow_html_tables: "Pozwalaj tabelom być zamieszczanym w Markdown przy użyciu tagów HTML. TABLE, THEAD, TD, TR, TH będą dozwolone (wymaga pełnego rebake na wszystkich starych postach zawierających tabele)."
post_undo_action_window_mins: "Przez tyle minut użytkownicy mogą cofnąć swoje ostatnie działania przy danym poście (lajki, flagowanie, itd.)."
must_approve_users: "Zespół musi zaakceptować wszystkie nowe konta zanim uzyskają dostęp do serwisu. UWAGA: włączenie tego dla już udostępnionej strony sprawi, że zostanie odebrany dostęp wszystkim istniejącym użytkownikom spoza zespołu."
pending_users_reminder_delay: "Powiadomić moderatorów jeżeli nowi użytkownicy czekali na zatwierdzenie dłużej niż his mamy godzin. Ustaw -1 aby wyłączyć powiadomienia. "

View File

@ -802,7 +802,6 @@ pt:
notify_mods_when_user_blocked: "Se um utilizador for bloqueado de forma automática, enviar uma mensagem a todos os moderadores."
flag_sockpuppets: "Se um novo utilizador responde a um tópico a partir do mesmo endereço IP do novo utilizador que iniciou o tópico, sinalizar ambas as mensagens como potencial spam."
traditional_markdown_linebreaks: "Utilize tradicionais quebras de linha no Markdown, que requer dois espaços no final para uma quebra de linha."
allow_html_tables: "Permitir inserção de tabelas em Markdown utilizando tags HTML. TABLE,THEAD, TD, TR,TH fazem parte da lista branca (requer que todas as mensagens antigas que contém tabelas sejam refeitas)"
post_undo_action_window_mins: "Número de minutos durante o qual os utilizadores têm permissão para desfazer ações numa mensagem (gostos, sinalizações, etc)."
must_approve_users: "O pessoal deve aprovar todas as novas contas de utilizador antes destas terem permissão para aceder ao sítio. AVISO: ativar isto para um sítio ativo irá revogar o acesso aos utilizadores existentes que não fazem parte do pessoal!"
pending_users_reminder_delay: "Notificar moderadores se novos utilizadores estiverem à espera de aprovação por mais que esta quantidade de horas. Configurar com -1 para desativar notificações."

View File

@ -878,7 +878,6 @@ ro:
notify_mods_when_user_blocked: "Dacă un utilizator este blocat automat, trimite un mesaj tuturor moderatorilor."
flag_sockpuppets: "Dacă un utilizator nou răspunde unui subiect de la același IP ca utilizatorul ce a pornit subiectul, marchează ambele postări ca potențial spam."
traditional_markdown_linebreaks: "Folosește întreruperi de rând tradiționale în Markdown, ceea ce necesită două spații pentru un capăt de rând. "
allow_html_tables: "Permite introducerea de tabele în Markdown prin folosirea de etichete HTML. HEAD, TD, TR, TH vor fi autorizate (necesită un rebake pe toate postările vechi ce conțin tabele)"
post_undo_action_window_mins: "Numărul de minute în care utilizatorii pot anula acțiunile recente asupra unei postări (aprecieri, marcări cu marcaje de avertizare, etc)."
must_approve_users: "Membrii echipei trebuie să aprobe toate conturile noilor utilizatori înainte ca aceștia să poată accesa site-ul. ATENȚIE: activarea acestei opțiuni pentru un site în producție va revoca accesul tuturor utilizatorilor care nu sunt membri ai echipei!"
pending_users_reminder_delay: "Notifică moderatorii dacă noii utilizatori sunt în așteptarea aprobării de mai mult de atâtea ore. Setează la -1 pentru a dezactiva notificările."

View File

@ -761,7 +761,6 @@ sk:
notify_mods_when_user_blocked: "Ak je používateľ automaticky zablokovaný, pošli správu všetkým moderátorom."
flag_sockpuppets: "Ak nový používateľ odpovedá na tému z rovnakej IP adresy, ako nový používateľ, ktorý danú tému vytvoril, označ oba ich príspevky ako potencionálny spam."
traditional_markdown_linebreaks: "V Markdown použiť tradičné oddeľovače riadkov, čo vyžaduje dve koncové medzery ako oddeľovač riadku."
allow_html_tables: "V Markdown umožniť použitie tabuliek pomocou HTML značiek. TABLE, THEAD, TD, TR, TH budú umožnené (vyžaduje \"full rebake\" na všetkých starých príspevkoch ktoré obsahujú tabuľky)"
post_undo_action_window_mins: "Počet minút počas ktorých môžu používatelia zrušiť poslednú akciu na príspevku (\"Páči sa\", označenie, atď..)."
must_approve_users: "Obsluha musí povoliť účty všetkým novým používateľom skôr než im bude povolený prístup na stránku. UPOZORNENIE: zapnutie na živej stránke spôsobí zrušenie prístupu pre existujúcich používateľov, okrem obsluhy!"
pending_users_reminder_delay: "Upozorni moderátora ak nový používateľ čaká na schválenie dlhšie ako tento počet hodín. Nastavte -1 pre vypnutie upozornenia."

View File

@ -818,7 +818,6 @@ sv:
notify_mods_when_user_blocked: "Om en användare blockeras automatiskt, skicka ett meddelande till alla moderatorer."
flag_sockpuppets: "Flagga båda användarnas inlägg som potentiell skräppost om en ny användare svarar på ett ämne från samma IP-adress som den andra nya användaren som skapade ämnet."
traditional_markdown_linebreaks: "Använd vanliga radmatningar i Markdown, vilka kräver 2 avslutande mellanslag för en radmatning."
allow_html_tables: "Tillåt tabeller att läggas in i Markdown genom användning av HTML-taggar. TABLE, THEAD, TD, TR, TH kommer att vitlistas (kräver full uppdatering/rebake av alla gamla inlägg som innehåller tabeller)"
post_undo_action_window_mins: "Antal minuter som en användare tillåts att upphäva handlingar på ett inlägg som gjorts nyligen (gillning, flaggning osv)."
must_approve_users: "Personal måste godkänna alla nya användarkonton innan de tillåts använda webbplatsen. VARNING: om det tillåts när webbplatsen är live så kommer det att upphäva tillgång för alla existerande användare som inte är personal!"
pending_users_reminder_delay: "Notifiera moderatorer om nya användare har väntat på godkännande längre än så här många timmar. Ange -1 för att inaktivera notifikationer. "

View File

@ -705,7 +705,6 @@ tr_TR:
notify_mods_when_user_blocked: "Eğer bir kullanıcı otomatik olarak engellendiyse, tüm moderatörlere ileti yolla."
flag_sockpuppets: "Eğer, yeni kullanıcı konuya, konuyu başlatan yeni kullanıcı ile aynı IP adresinden cevap yazarsa, her iki gönderiyi de potansiyel istenmeyen olarak bildir. "
traditional_markdown_linebreaks: "Markdown'da, satır sonundan önce yazının sağında iki tane boşluk gerektiren, geleneksel satır sonu metodunu kullan."
allow_html_tables: "Tabloların HTML etiketleri kullanılarak Markdown ile oluşturulmasına izin verin. TABLE, THEAD, TD, TR, TH kabul edilir (tablo içeren tüm eski gönderilerin yenilenmesini gerektirir) "
post_undo_action_window_mins: "Bir gönderide yapılan yeni eylemlerin (beğenme, bildirme vb) geri alınabileceği zaman, dakika olarak"
must_approve_users: "Siteye erişimlerine izin verilmeden önce tüm yeni kullanıcı hesaplarının görevliler tarafından onaylanması gerekir. UYARI: yayındaki bir site için bunu etkinleştirmek görevli olmayan hesapların erişimini iptal edecek."
pending_users_reminder_delay: "Belirtilen saatten daha uzun bir süredir onay bekleyen yeni kullanıcılar mevcutsa moderatörleri bilgilendir. Bilgilendirmeyi devre dışı bırakmak için -1 girin."

View File

@ -681,7 +681,6 @@ vi:
notify_mods_when_user_blocked: "Nếu một thành viên được khóa tự động, gửi tin nhắn đến tất cả các điều hành viên."
flag_sockpuppets: "Nếu thành viên mới trả lời chủ đề có cùng địa chỉ IP với thành viên mới tạo chủ đề, đánh dấu các bài viết của họ là spam tiềm năng."
traditional_markdown_linebreaks: "Sử dụng ngắt dòng truyền thống trong Markdown, đòi hỏi hai khoảng trống kế tiếp cho một ngắt dòng."
allow_html_tables: "Cho phép nhập bảng trong Markdown sử dụng các thẻ HTML. TABLE, THEAD, TD, TR, TH sẽ được sử dụng (đòi hỏi thực hiện lại cho các bài viết cũ có chứa bảng)"
post_undo_action_window_mins: "Số phút thành viên được phép làm lại các hành động gần đây với bài viết (like, đánh dấu...)."
must_approve_users: "Quản trị viên phải duyệt tất cả các tài khoản thành viên mới trước khi họ có quyền truy cập website. LƯU Ý: bật tính năng này trên site đang hoạt động sẽ hủy bỏ quyền truy cập đối với các tài khoản thành viên hiện tại!"
pending_users_reminder_delay: "Thông báo cho quản trị viên nếu thành viên mới đã chờ duyệt lâu hơn số giờ được thiết lập ở đây, đặt là -1 để tắt thông báo."

View File

@ -881,7 +881,6 @@ zh_CN:
notify_mods_when_user_blocked: "如果一个用户被自动封禁了,发送一个私信给所有管理员。"
flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。"
traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行"
allow_html_tables: "允许在输入 Markdown 文本时使用表格 HTML 标签。标签 TABLE、THEAD、TD、TR、TH 将被允许使用,即白名单这些标签(需要重置所有包含表格的老帖子的 HTML"
post_undo_action_window_mins: "允许用户在帖子上进行撤销操作(赞、标记等)所需等待的间隔分钟数"
must_approve_users: "新用户在被允许访问站点前需要由管理人员批准。警告:在运行的站点中启用将解除所有非管理人员用户的访问权限!"
pending_users_reminder_delay: "如果新用户等待批准时间超过此小时设置则通知版主。设置 -1 关闭通知。"

View File

@ -832,7 +832,6 @@ zh_TW:
notify_mods_when_user_blocked: "若有用戶被自動封鎖,將發送訊息給所有板主。"
flag_sockpuppets: "如果一個新用戶開始了一個主題,並且同時另一個新用戶以同一個 IP 在該主題回復,他們所有的帖子都將被自動標記為垃圾。"
traditional_markdown_linebreaks: "在 Markdown 中使用傳統的換行符號,即用兩個行末空格來換行"
allow_html_tables: "允許在輸入 Markdown 文本時使用表格 HTML 標籤。標籤 TABLE、THEAD、TD、TR、TH 將被允許使用,即白名單這些標籤(需要重置所有包含表格的老帖子的 HTML"
post_undo_action_window_mins: "允許用戶在帖子上進行撤銷操作(讚、標記等)所需等待的時間分隔(分鐘)"
must_approve_users: "新用戶在被允許訪問站點前需要由管理人員批准。警告:在運行的站點中啟用將解除所有非管理人員用戶的訪問權限!"
pending_users_reminder_delay: "如果新用戶等待批准時間超過此小時設置則通知版主。設置 -1 關閉通知。"

View File

@ -492,19 +492,12 @@ posting:
delete_removed_posts_after:
client: true
default: 24
enable_experimental_markdown_it:
client: true
default: false
shadowed_by_global: true
traditional_markdown_linebreaks:
client: true
default: false
enable_markdown_typographer:
client: true
default: false
allow_html_tables:
client: true
default: false
default: true
suppress_reply_directly_below:
client: true
default: true

View File

@ -80,11 +80,7 @@ module PrettyText
ctx_load(ctx, "#{Rails.root}/app/assets/javascripts/discourse-loader.js")
ctx_load(ctx, "vendor/assets/javascripts/lodash.js")
ctx_load_manifest(ctx, "pretty-text-bundle.js")
if SiteSetting.enable_experimental_markdown_it
ctx_load_manifest(ctx, "markdown-it-bundle.js")
end
root_path = "#{Rails.root}/app/assets/javascripts/"
apply_es6_file(ctx, root_path, "discourse/lib/utilities")
@ -152,13 +148,6 @@ module PrettyText
paths[:S3BaseUrl] = Discourse.store.absolute_base_url
end
if SiteSetting.enable_experimental_markdown_it
# defer load markdown it
unless context.eval("window.markdownit")
ctx_load_manifest(context, "markdown-it-bundle.js")
end
end
custom_emoji = {}
Emoji.custom.map { |e| custom_emoji[e.name] = e.url }

View File

@ -7,8 +7,6 @@ __utils = require('discourse/lib/utilities');
__emojiUnicodeReplacer = null;
__setUnicode = function(replacements) {
require('pretty-text/engines/discourse-markdown/emoji').setUnicodeReplacements(replacements);
let unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g");
__emojiUnicodeReplacer = function(text) {

View File

@ -53,7 +53,6 @@ class ImportScripts::Lithium < ImportScripts::Base
def execute
SiteSetting.allow_html_tables = true
@max_start_id = Post.maximum(:id)
import_categories

View File

@ -5,7 +5,7 @@ require 'html_normalize'
describe PrettyText do
before do
SiteSetting.enable_experimental_markdown_it = true
SiteSetting.enable_markdown_typographer = false
end
def n(html)

View File

@ -42,9 +42,6 @@ QUnit.assert.cookedPara = function(input, expected, message) {
};
QUnit.test("buildOptions", assert => {
assert.ok(buildOptions({ siteSettings: { allow_html_tables: true } }).features.table, 'tables enabled');
assert.ok(!buildOptions({ siteSettings: { allow_html_tables: false } }).features.table, 'tables disabled');
assert.ok(buildOptions({ siteSettings: { enable_emoji: true } }).features.emoji, 'emoji enabled');
assert.ok(!buildOptions({ siteSettings: { enable_emoji: false } }).features.emoji, 'emoji disabled');
});

File diff suppressed because it is too large Load Diff