2017-05-09 18:01:43 +02:00
|
|
|
// This contains zulip's frontend markdown implementation; see
|
2017-11-08 17:55:36 +01:00
|
|
|
// docs/subsystems/markdown.md for docs on our Markdown syntax. The other
|
2017-05-09 18:01:43 +02:00
|
|
|
// main piece in rendering markdown client-side is
|
|
|
|
// static/third/marked/lib/marked.js, which we have significantly
|
|
|
|
// modified from the original implementation.
|
|
|
|
|
2018-11-30 00:48:13 +01:00
|
|
|
// Docs: https://zulip.readthedocs.io/en/latest/subsystems/markdown.html
|
2017-05-09 18:01:43 +02:00
|
|
|
|
2020-02-12 06:28:13 +01:00
|
|
|
const realm_filter_map = new Map();
|
2019-11-02 00:06:25 +01:00
|
|
|
let realm_filter_list = [];
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
// Regexes that match some of our common bugdown markup
|
2019-11-02 00:06:25 +01:00
|
|
|
const backend_only_markdown_re = [
|
2017-05-09 18:01:43 +02:00
|
|
|
// Inline image previews, check for contiguous chars ending in image suffix
|
|
|
|
// To keep the below regexes simple, split them out for the end-of-message case
|
|
|
|
|
2017-12-27 19:46:57 +01:00
|
|
|
/[^\s]*(?:(?:\.bmp|\.gif|\.jpg|\.jpeg|\.png|\.webp)\)?)\s+/m,
|
|
|
|
/[^\s]*(?:(?:\.bmp|\.gif|\.jpg|\.jpeg|\.png|\.webp)\)?)$/m,
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
// Twitter and youtube links are given previews
|
|
|
|
|
|
|
|
/[^\s]*(?:twitter|youtube).com\/[^\s]*/,
|
|
|
|
];
|
|
|
|
|
2019-02-15 23:24:26 +01:00
|
|
|
// Helper function to update a mentioned user's name.
|
|
|
|
exports.set_name_in_mention_element = function (element, name) {
|
|
|
|
if ($(element).hasClass('silent')) {
|
|
|
|
$(element).text(name);
|
|
|
|
} else {
|
|
|
|
$(element).text("@" + name);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-02-15 15:21:32 +01:00
|
|
|
exports.translate_emoticons_to_names = (text) => {
|
|
|
|
// Translates emoticons in a string to their colon syntax.
|
|
|
|
let translated = text;
|
|
|
|
let replacement_text;
|
|
|
|
const terminal_symbols = ',.;?!()[] "\'\n\t'; // From composebox_typeahead
|
|
|
|
const symbols_except_space = terminal_symbols.replace(' ', '');
|
|
|
|
|
|
|
|
const emoticon_replacer = function (match, g1, offset, str) {
|
|
|
|
const prev_char = str[offset - 1];
|
|
|
|
const next_char = str[offset + match.length];
|
|
|
|
|
|
|
|
const symbol_at_start = terminal_symbols.includes(prev_char);
|
|
|
|
const symbol_at_end = terminal_symbols.includes(next_char);
|
|
|
|
const non_space_at_start = symbols_except_space.includes(prev_char);
|
|
|
|
const non_space_at_end = symbols_except_space.includes(next_char);
|
|
|
|
const valid_start = symbol_at_start || offset === 0;
|
|
|
|
const valid_end = symbol_at_end || offset === str.length - match.length;
|
|
|
|
|
|
|
|
if (non_space_at_start && non_space_at_end) { // Hello!:)?
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
if (valid_start && valid_end) {
|
|
|
|
return replacement_text;
|
|
|
|
}
|
|
|
|
return match;
|
|
|
|
};
|
|
|
|
|
|
|
|
for (const translation of emoji.get_emoticon_translations()) {
|
|
|
|
// We can't pass replacement_text directly into
|
|
|
|
// emoticon_replacer, because emoticon_replacer is
|
|
|
|
// a callback for `replace()`. Instead we just mutate
|
|
|
|
// the `replacement_text` that the function closes on.
|
|
|
|
replacement_text = translation.replacement_text;
|
|
|
|
translated = translated.replace(translation.regex, emoticon_replacer);
|
|
|
|
}
|
|
|
|
|
|
|
|
return translated;
|
|
|
|
};
|
|
|
|
|
2017-07-29 02:51:33 +02:00
|
|
|
exports.contains_backend_only_syntax = function (content) {
|
2017-05-09 18:01:43 +02:00
|
|
|
// Try to guess whether or not a message has bugdown in it
|
|
|
|
// If it doesn't, we can immediately render it client-side
|
2020-02-08 05:31:13 +01:00
|
|
|
const markedup = backend_only_markdown_re.find(re => re.test(content));
|
2017-07-30 21:07:59 +02:00
|
|
|
|
|
|
|
// If a realm filter doesn't start with some specified characters
|
|
|
|
// then don't render it locally. It is workaround for the fact that
|
|
|
|
// javascript regex doesn't support lookbehind.
|
2020-02-08 05:31:13 +01:00
|
|
|
const false_filter_match = realm_filter_list.find(re => {
|
2019-11-02 00:06:25 +01:00
|
|
|
const pattern = /(?:[^\s'"\(,:<])/.source + re[0].source + /(?![\w])/.source;
|
|
|
|
const regex = new RegExp(pattern);
|
2017-07-30 21:07:59 +02:00
|
|
|
return regex.test(content);
|
|
|
|
});
|
|
|
|
return markedup !== undefined || false_filter_match !== undefined;
|
2017-05-09 18:01:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.apply_markdown = function (message) {
|
2017-12-16 23:25:31 +01:00
|
|
|
message_store.init_booleans(message);
|
2017-05-09 18:01:43 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const options = {
|
2019-01-08 09:30:19 +01:00
|
|
|
userMentionHandler: function (name, silently) {
|
2020-02-15 16:25:00 +01:00
|
|
|
if (name === 'all' || name === 'everyone' || name === 'stream') {
|
|
|
|
message.mentioned = true;
|
|
|
|
return '<span class="user-mention" data-user-id="*">' +
|
|
|
|
'@' + name +
|
|
|
|
'</span>';
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let person = people.get_by_name(name);
|
2018-08-19 03:39:57 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const id_regex = /(.+)\|(\d+)$/g; // For @**user|id** syntax
|
|
|
|
const match = id_regex.exec(name);
|
2018-08-19 03:39:57 +02:00
|
|
|
if (match) {
|
2019-12-29 15:07:05 +01:00
|
|
|
const user_id = parseInt(match[2], 10);
|
|
|
|
if (people.is_known_user_id(user_id)) {
|
2020-02-05 14:30:59 +01:00
|
|
|
person = people.get_by_user_id(user_id);
|
2018-08-19 03:39:57 +02:00
|
|
|
if (person.full_name !== match[1]) { // Invalid Syntax
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-15 16:51:37 +01:00
|
|
|
if (!person) {
|
|
|
|
// This is nothing to be concerned about--the users
|
|
|
|
// are allowed to hand-type mentions and they may
|
|
|
|
// have had a typo in the name.
|
|
|
|
return;
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
2020-02-15 16:51:37 +01:00
|
|
|
|
|
|
|
// HAPPY PATH! Note that we not only need to return the
|
|
|
|
// appropriate HTML snippet here; we also want to update
|
|
|
|
// flags on the message itself that get used by the message
|
|
|
|
// view code and possibly our filtering code.
|
|
|
|
|
|
|
|
if (people.my_current_user_id() === person.user_id && !silently) {
|
|
|
|
message.mentioned = true;
|
|
|
|
message.mentioned_me_directly = true;
|
|
|
|
}
|
|
|
|
let str = '';
|
|
|
|
if (silently) {
|
|
|
|
str += '<span class="user-mention silent" data-user-id="' + person.user_id + '">';
|
|
|
|
} else {
|
|
|
|
str += '<span class="user-mention" data-user-id="' + person.user_id + '">@';
|
|
|
|
}
|
|
|
|
return str + _.escape(person.full_name) + '</span>';
|
2017-05-09 18:01:43 +02:00
|
|
|
},
|
2017-11-22 09:11:07 +01:00
|
|
|
groupMentionHandler: function (name) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const group = user_groups.get_user_group_from_name(name);
|
2017-11-22 09:11:07 +01:00
|
|
|
if (group !== undefined) {
|
|
|
|
if (user_groups.is_member_of(group.id, people.my_current_user_id())) {
|
2017-12-16 23:25:31 +01:00
|
|
|
message.mentioned = true;
|
2017-11-22 09:11:07 +01:00
|
|
|
}
|
|
|
|
return '<span class="user-group-mention" data-user-group-id="' + group.id + '">' +
|
2019-12-13 01:38:49 +01:00
|
|
|
'@' + _.escape(group.name) +
|
2017-11-22 09:11:07 +01:00
|
|
|
'</span>';
|
|
|
|
}
|
2018-03-13 13:04:16 +01:00
|
|
|
return;
|
2017-11-22 09:11:07 +01:00
|
|
|
},
|
2019-01-08 11:30:13 +01:00
|
|
|
silencedMentionHandler: function (quote) {
|
|
|
|
// Silence quoted mentions.
|
2019-11-02 00:06:25 +01:00
|
|
|
const user_mention_re = /<span.*user-mention.*data-user-id="(\d+|\*)"[^>]*>@/gm;
|
2019-01-08 11:30:13 +01:00
|
|
|
quote = quote.replace(user_mention_re, function (match) {
|
2019-02-15 20:58:54 +01:00
|
|
|
match = match.replace(/"user-mention"/g, '"user-mention silent"');
|
|
|
|
match = match.replace(/>@/g, '>');
|
|
|
|
return match;
|
2019-01-08 11:30:13 +01:00
|
|
|
});
|
|
|
|
// In most cases, if you are being mentioned in the message you're quoting, you wouldn't
|
|
|
|
// mention yourself outside of the blockquote (and, above it). If that you do that, the
|
|
|
|
// following mentioned status is false; the backend rendering is authoritative and the
|
|
|
|
// only side effect is the lack red flash on immediately sending the message.
|
|
|
|
message.mentioned = false;
|
|
|
|
message.mentioned_me_directly = false;
|
|
|
|
return quote;
|
|
|
|
},
|
2017-05-09 18:01:43 +02:00
|
|
|
};
|
2019-08-21 18:48:59 +02:00
|
|
|
// Our python-markdown processor appends two \n\n to input
|
2017-05-09 18:01:43 +02:00
|
|
|
message.content = marked(message.raw_content + '\n\n', options).trim();
|
2019-12-03 15:29:44 +01:00
|
|
|
message.is_me_message = exports.is_status_message(message.raw_content);
|
2017-05-09 18:01:43 +02:00
|
|
|
};
|
|
|
|
|
2018-11-13 16:19:59 +01:00
|
|
|
exports.add_topic_links = function (message) {
|
2017-05-09 18:01:43 +02:00
|
|
|
if (message.type !== 'stream') {
|
2020-02-14 13:39:04 +01:00
|
|
|
message.topic_links = [];
|
2017-05-09 18:01:43 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-02-14 14:45:49 +01:00
|
|
|
const topic = message.topic;
|
2019-11-02 00:06:25 +01:00
|
|
|
let links = [];
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const realm_filter of realm_filter_list) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const pattern = realm_filter[0];
|
|
|
|
const url = realm_filter[1];
|
|
|
|
let match;
|
2018-11-13 16:41:18 +01:00
|
|
|
while ((match = pattern.exec(topic)) !== null) {
|
2019-11-02 00:06:25 +01:00
|
|
|
let link_url = url;
|
|
|
|
const matched_groups = match.slice(1);
|
|
|
|
let i = 0;
|
2017-05-09 18:01:43 +02:00
|
|
|
while (i < matched_groups.length) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const matched_group = matched_groups[i];
|
|
|
|
const current_group = i + 1;
|
|
|
|
const back_ref = "\\" + current_group;
|
2017-05-09 18:01:43 +02:00
|
|
|
link_url = link_url.replace(back_ref, matched_group);
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
links.push(link_url);
|
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2019-05-25 16:10:30 +02:00
|
|
|
|
|
|
|
// Also make raw urls navigable
|
2019-11-02 00:06:25 +01:00
|
|
|
const url_re = /\b(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g; // Slightly modified from third/marked.js
|
|
|
|
const match = topic.match(url_re);
|
2019-05-25 16:10:30 +02:00
|
|
|
if (match) {
|
|
|
|
links = links.concat(match);
|
|
|
|
}
|
|
|
|
|
2020-02-14 13:39:04 +01:00
|
|
|
message.topic_links = links;
|
2017-05-09 18:01:43 +02:00
|
|
|
};
|
|
|
|
|
2019-12-03 15:29:44 +01:00
|
|
|
exports.is_status_message = function (raw_content) {
|
2020-01-28 15:26:02 +01:00
|
|
|
return raw_content.startsWith('/me ');
|
2018-01-21 19:27:36 +01:00
|
|
|
};
|
|
|
|
|
2019-01-16 10:11:30 +01:00
|
|
|
function make_emoji_span(codepoint, title, alt_text) {
|
2019-01-14 08:45:37 +01:00
|
|
|
return '<span aria-label="' + title + '"' +
|
|
|
|
' class="emoji emoji-' + codepoint + '"' +
|
|
|
|
' role="img" title="' + title + '">' + alt_text +
|
2019-01-16 10:11:30 +01:00
|
|
|
'</span>';
|
|
|
|
}
|
|
|
|
|
2017-05-09 18:01:43 +02:00
|
|
|
function handleUnicodeEmoji(unicode_emoji) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const codepoint = unicode_emoji.codePointAt(0).toString(16);
|
2020-02-15 13:19:42 +01:00
|
|
|
const emoji_name = emoji.get_emoji_name(codepoint);
|
|
|
|
if (emoji_name) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const alt_text = ':' + emoji_name + ':';
|
|
|
|
const title = emoji_name.split("_").join(" ");
|
2019-01-16 10:11:30 +01:00
|
|
|
return make_emoji_span(codepoint, title, alt_text);
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
return unicode_emoji;
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleEmoji(emoji_name) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const alt_text = ':' + emoji_name + ':';
|
|
|
|
const title = emoji_name.split("_").join(" ");
|
2020-02-15 13:19:42 +01:00
|
|
|
|
|
|
|
// Zulip supports both standard/unicode emoji, served by a
|
|
|
|
// spritesheet and custom realm-specific emoji (served by URL).
|
|
|
|
// We first check if this is a realm emoji, and if so, render it.
|
|
|
|
//
|
|
|
|
// Otherwise we'll look at unicode emoji to render with an emoji
|
|
|
|
// span using the spritesheet; and if it isn't one of those
|
|
|
|
// either, we pass through the plain text syntax unmodified.
|
|
|
|
const emoji_url = emoji.get_realm_emoji_url(emoji_name);
|
|
|
|
|
|
|
|
if (emoji_url) {
|
2017-09-27 19:39:42 +02:00
|
|
|
return '<img alt="' + alt_text + '"' +
|
2017-05-09 18:01:43 +02:00
|
|
|
' class="emoji" src="' + emoji_url + '"' +
|
2017-06-09 10:30:24 +02:00
|
|
|
' title="' + title + '">';
|
2020-02-15 13:19:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const codepoint = emoji.get_emoji_codepoint(emoji_name);
|
|
|
|
if (codepoint) {
|
2019-01-16 10:11:30 +01:00
|
|
|
return make_emoji_span(codepoint, title, alt_text);
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
2020-02-15 13:19:42 +01:00
|
|
|
|
2017-09-27 19:39:42 +02:00
|
|
|
return alt_text;
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleAvatar(email) {
|
|
|
|
return '<img alt="' + email + '"' +
|
|
|
|
' class="message_body_gravatar" src="/avatar/' + email + '?s=30"' +
|
|
|
|
' title="' + email + '">';
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleStream(streamName) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const stream = stream_data.get_sub(streamName);
|
2017-05-09 18:01:43 +02:00
|
|
|
if (stream === undefined) {
|
2018-03-13 13:04:16 +01:00
|
|
|
return;
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const href = hash_util.by_stream_uri(stream.stream_id);
|
2017-05-09 18:01:43 +02:00
|
|
|
return '<a class="stream" data-stream-id="' + stream.stream_id + '" ' +
|
2019-07-12 00:09:38 +02:00
|
|
|
'href="/' + href + '"' +
|
2019-12-13 01:38:49 +01:00
|
|
|
'>' + '#' + _.escape(stream.name) + '</a>';
|
2019-06-21 20:47:09 +02:00
|
|
|
}
|
2017-05-09 18:01:43 +02:00
|
|
|
|
2019-06-21 20:47:09 +02:00
|
|
|
function handleStreamTopic(streamName, topic) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const stream = stream_data.get_sub(streamName);
|
2019-06-21 20:47:09 +02:00
|
|
|
if (stream === undefined || !topic) {
|
|
|
|
return;
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const href = hash_util.by_stream_topic_uri(stream.stream_id, topic);
|
2019-12-13 01:38:49 +01:00
|
|
|
const text = '#' + _.escape(stream.name) + ' > ' + _.escape(topic);
|
2019-06-21 20:47:09 +02:00
|
|
|
return '<a class="stream-topic" data-stream-id="' + stream.stream_id + '" ' +
|
2019-07-12 00:09:38 +02:00
|
|
|
'href="/' + href + '"' + '>' + text + '</a>';
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleRealmFilter(pattern, matches) {
|
2020-02-12 06:28:13 +01:00
|
|
|
let url = realm_filter_map.get(pattern);
|
2017-05-09 18:01:43 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let current_group = 1;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const match of matches) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const back_ref = "\\" + current_group;
|
2017-05-09 18:01:43 +02:00
|
|
|
url = url.replace(back_ref, match);
|
|
|
|
current_group += 1;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleTex(tex, fullmatch) {
|
|
|
|
try {
|
|
|
|
return katex.renderToString(tex);
|
|
|
|
} catch (ex) {
|
2020-01-28 15:26:02 +01:00
|
|
|
if (ex.message.startsWith('KaTeX parse error')) { // TeX syntax error
|
2019-12-13 01:38:49 +01:00
|
|
|
return '<span class="tex-error">' + _.escape(fullmatch) + '</span>';
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
blueslip.error(ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function python_to_js_filter(pattern, url) {
|
|
|
|
// Converts a python named-group regex to a javascript-compatible numbered
|
|
|
|
// group regex... with a regex!
|
2019-11-02 00:06:25 +01:00
|
|
|
const named_group_re = /\(?P<([^>]+?)>/g;
|
|
|
|
let match = named_group_re.exec(pattern);
|
|
|
|
let current_group = 1;
|
2017-05-09 18:01:43 +02:00
|
|
|
while (match) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const name = match[1];
|
2017-05-09 18:01:43 +02:00
|
|
|
// Replace named group with regular matching group
|
|
|
|
pattern = pattern.replace('(?P<' + name + '>', '(');
|
|
|
|
// Replace named reference in url to numbered reference
|
|
|
|
url = url.replace('%(' + name + ')s', '\\' + current_group);
|
|
|
|
|
2019-02-11 22:54:18 +01:00
|
|
|
// Reset the RegExp state
|
|
|
|
named_group_re.lastIndex = 0;
|
2017-05-09 18:01:43 +02:00
|
|
|
match = named_group_re.exec(pattern);
|
|
|
|
|
|
|
|
current_group += 1;
|
|
|
|
}
|
|
|
|
// Convert any python in-regex flags to RegExp flags
|
2019-11-02 00:06:25 +01:00
|
|
|
let js_flags = 'g';
|
|
|
|
const inline_flag_re = /\(\?([iLmsux]+)\)/;
|
2017-05-09 18:01:43 +02:00
|
|
|
match = inline_flag_re.exec(pattern);
|
|
|
|
|
|
|
|
// JS regexes only support i (case insensitivity) and m (multiline)
|
|
|
|
// flags, so keep those and ignore the rest
|
|
|
|
if (match) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const py_flags = match[1].split("");
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const flag of py_flags) {
|
js: Convert a.indexOf(…) !== -1 to a.includes(…).
Babel polyfills this for us for Internet Explorer.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
recast.visit(ast, {
visitBinaryExpression(path) {
const { operator, left, right } = path.node;
if (
n.CallExpression.check(left) &&
n.MemberExpression.check(left.callee) &&
!left.callee.computed &&
n.Identifier.check(left.callee.property) &&
left.callee.property.name === "indexOf" &&
left.arguments.length === 1 &&
checkExpression(left.arguments[0]) &&
((["===", "!==", "==", "!=", ">", "<="].includes(operator) &&
n.UnaryExpression.check(right) &&
right.operator == "-" &&
n.Literal.check(right.argument) &&
right.argument.value === 1) ||
([">=", "<"].includes(operator) &&
n.Literal.check(right) &&
right.value === 0))
) {
const test = b.callExpression(
b.memberExpression(left.callee.object, b.identifier("includes")),
[left.arguments[0]]
);
path.replace(
["!==", "!=", ">", ">="].includes(operator)
? test
: b.unaryExpression("!", test)
);
changed = true;
}
this.traverse(path);
},
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 04:55:06 +01:00
|
|
|
if ("im".includes(flag)) {
|
2017-05-09 18:01:43 +02:00
|
|
|
js_flags += flag;
|
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
|
|
|
|
2017-05-09 18:01:43 +02:00
|
|
|
pattern = pattern.replace(inline_flag_re, "");
|
|
|
|
}
|
2017-07-30 21:07:59 +02:00
|
|
|
// Ideally we should have been checking that realm filters
|
|
|
|
// begin with certain characters but since there is no
|
|
|
|
// support for negative lookbehind in javascript, we check
|
|
|
|
// for this condition in `contains_backend_only_syntax()`
|
|
|
|
// function. If the condition is satisfied then the message
|
|
|
|
// is rendered locally, otherwise, we return false there and
|
|
|
|
// message is rendered on the backend which has proper support
|
|
|
|
// for negative lookbehind.
|
|
|
|
pattern = pattern + /(?![\w])/.source;
|
2019-11-02 00:06:25 +01:00
|
|
|
let final_regex = null;
|
2019-02-12 22:30:57 +01:00
|
|
|
try {
|
|
|
|
final_regex = new RegExp(pattern, js_flags);
|
|
|
|
} catch (ex) {
|
|
|
|
// We have an error computing the generated regex syntax.
|
|
|
|
// We'll ignore this realm filter for now, but log this
|
|
|
|
// failure for debugging later.
|
|
|
|
blueslip.error('python_to_js_filter: ' + ex.message);
|
|
|
|
}
|
|
|
|
return [final_regex, url];
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
|
2020-02-17 14:43:59 +01:00
|
|
|
exports.update_realm_filter_rules = function (realm_filters) {
|
2017-05-09 18:01:43 +02:00
|
|
|
// Update the marked parser with our particular set of realm filters
|
2020-02-12 06:28:13 +01:00
|
|
|
realm_filter_map.clear();
|
2017-05-09 18:01:43 +02:00
|
|
|
realm_filter_list = [];
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const marked_rules = [];
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
2020-02-12 06:28:13 +01:00
|
|
|
for (const [pattern, url] of realm_filters) {
|
|
|
|
const [regex, final_url] = python_to_js_filter(pattern, url);
|
|
|
|
if (!regex) {
|
2019-02-12 22:30:57 +01:00
|
|
|
// Skip any realm filters that could not be converted
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
continue;
|
2019-02-12 22:30:57 +01:00
|
|
|
}
|
2017-05-09 18:01:43 +02:00
|
|
|
|
2020-02-12 06:28:13 +01:00
|
|
|
realm_filter_map.set(regex, final_url);
|
|
|
|
realm_filter_list.push([regex, final_url]);
|
|
|
|
marked_rules.push(regex);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
marked.InlineLexer.rules.zulip.realm_filters = marked_rules;
|
|
|
|
};
|
|
|
|
|
2020-02-17 14:49:17 +01:00
|
|
|
exports.initialize = function (realm_filters) {
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
function disable_markdown_regex(rules, name) {
|
|
|
|
rules[name] = {exec: function () {
|
2018-05-06 21:43:17 +02:00
|
|
|
return false;
|
|
|
|
}};
|
2017-05-09 18:01:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Configure the marked markdown parser for our usage
|
2019-11-02 00:06:25 +01:00
|
|
|
const r = new marked.Renderer();
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
// No <code> around our code blocks instead a codehilite <div> and disable
|
|
|
|
// class-specific highlighting.
|
|
|
|
r.code = function (code) {
|
|
|
|
return '<div class="codehilite"><pre>'
|
2019-12-13 01:38:49 +01:00
|
|
|
+ _.escape(code)
|
2017-05-09 18:01:43 +02:00
|
|
|
+ '\n</pre></div>\n\n\n';
|
|
|
|
};
|
|
|
|
|
|
|
|
// Our links have title= and target=_blank
|
|
|
|
r.link = function (href, title, text) {
|
|
|
|
title = title || href;
|
2019-08-11 13:34:24 +02:00
|
|
|
if (!text.trim()) {
|
|
|
|
text = href;
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const out = '<a href="' + href + '"' + ' target="_blank" title="' +
|
2017-05-09 18:01:43 +02:00
|
|
|
title + '"' + '>' + text + '</a>';
|
|
|
|
return out;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Put a newline after a <br> in the generated HTML to match bugdown
|
|
|
|
r.br = function () {
|
|
|
|
return '<br>\n';
|
|
|
|
};
|
|
|
|
|
|
|
|
function preprocess_code_blocks(src) {
|
|
|
|
return fenced_code.process_fenced_code(src);
|
|
|
|
}
|
|
|
|
|
2018-01-15 19:36:32 +01:00
|
|
|
function preprocess_translate_emoticons(src) {
|
|
|
|
if (!page_params.translate_emoticons) {
|
|
|
|
return src;
|
|
|
|
}
|
|
|
|
|
|
|
|
// In this scenario, the message has to be from the user, so the only
|
|
|
|
// requirement should be that they have the setting on.
|
2020-02-15 15:21:32 +01:00
|
|
|
return exports.translate_emoticons_to_names(src);
|
2018-01-15 19:36:32 +01:00
|
|
|
}
|
|
|
|
|
2019-07-31 08:04:32 +02:00
|
|
|
// Disable lheadings
|
|
|
|
// We only keep the # Heading format.
|
2017-05-09 18:01:43 +02:00
|
|
|
disable_markdown_regex(marked.Lexer.rules.tables, 'lheading');
|
|
|
|
|
|
|
|
// Disable __strong__ (keeping **strong**)
|
|
|
|
marked.InlineLexer.rules.zulip.strong = /^\*\*([\s\S]+?)\*\*(?!\*)/;
|
|
|
|
|
|
|
|
// Make sure <del> syntax matches the backend processor
|
|
|
|
marked.InlineLexer.rules.zulip.del = /^(?!<\~)\~\~([^~]+)\~\~(?!\~)/;
|
|
|
|
|
|
|
|
// Disable _emphasis_ (keeping *emphasis*)
|
|
|
|
// Text inside ** must start and end with a word character
|
2018-04-22 19:53:04 +02:00
|
|
|
// to prevent mis-parsing things like "char **x = (char **)y"
|
2017-05-09 18:01:43 +02:00
|
|
|
marked.InlineLexer.rules.zulip.em = /^\*(?!\s+)((?:\*\*|[\s\S])+?)((?:[\S]))\*(?!\*)/;
|
|
|
|
|
|
|
|
// Disable autolink as (a) it is not used in our backend and (b) it interferes with @mentions
|
|
|
|
disable_markdown_regex(marked.InlineLexer.rules.zulip, 'autolink');
|
|
|
|
|
2020-02-17 14:49:17 +01:00
|
|
|
exports.update_realm_filter_rules(realm_filters);
|
2017-05-09 18:01:43 +02:00
|
|
|
|
|
|
|
// Tell our fenced code preprocessor how to insert arbitrary
|
|
|
|
// HTML into the output. This generated HTML is safe to not escape
|
|
|
|
fenced_code.set_stash_func(function (html) {
|
|
|
|
return marked.stashHtml(html, true);
|
|
|
|
});
|
|
|
|
|
|
|
|
marked.setOptions({
|
|
|
|
gfm: true,
|
|
|
|
tables: true,
|
|
|
|
breaks: true,
|
|
|
|
pedantic: false,
|
|
|
|
sanitize: true,
|
|
|
|
smartLists: true,
|
|
|
|
smartypants: false,
|
|
|
|
zulip: true,
|
|
|
|
emojiHandler: handleEmoji,
|
|
|
|
avatarHandler: handleAvatar,
|
|
|
|
unicodeEmojiHandler: handleUnicodeEmoji,
|
|
|
|
streamHandler: handleStream,
|
2019-06-21 20:47:09 +02:00
|
|
|
streamTopicHandler: handleStreamTopic,
|
2017-05-09 18:01:43 +02:00
|
|
|
realmFilterHandler: handleRealmFilter,
|
|
|
|
texHandler: handleTex,
|
|
|
|
renderer: r,
|
2018-01-15 19:36:32 +01:00
|
|
|
preprocessors: [
|
|
|
|
preprocess_code_blocks,
|
|
|
|
preprocess_translate_emoticons,
|
|
|
|
],
|
2017-05-09 18:01:43 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2019-10-25 09:45:13 +02:00
|
|
|
window.markdown = exports;
|