mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
markdown: Add helper configuration for mobile.
This refactoring is the first step toward sharing our markdown code with mobile. This focuses on the Zulip layer, not the underlying third party `marked` library. In this commit we do a one-time initialization to wire up the markdown functions, but after further discussions with Greg, it might make more sense to just pass in helpers on every use of markdown (which is generally only once per sent message). I'll address that in follow-up commits. Even though it looks like a pretty invasive change, you will note that we barely needed to modify the node tests to make this pass. And we have pretty decent test coverage here. All of the places where we used to depend on other Zulip modules now use helper functions that any client (e.g. mobile) can configure themselves. Or course, in the webapp, we configure these from modules like people/stream_data/hash_util/etc. Even in places where markdown used to deal directly with data structures from other modules, we now use functions. We may revisit this in a future commit, and we might just pass data directly for certain things. I decided to keep the helpers data structure completely flat, so we don't have ugly nested names like `helpers.emoji.get_emoji_codepoint`. Because of this, some of the names aren't 1:1, which I think is fine. For example, we map `user_groups.is_member_of` to `is_member_of_user_group`. It's likely that mobile already has different names for their versions of these functions, so trying for fake consistency would only help the webapp. In some cases, I think the webapp functions have names that could be improved, but we can clean that up in future commits, and since the names aren't coupled to markdown itself (i.e. only the config), we will be less constrained. It's worth noting that `marked` has an `options` data structure that it uses for configuration, but I didn't piggyback onto it, since the `marked` options are more at the lexing/parsing layer vs. the app-data layer stuff that our helpers mostly help with. Hopefully it's obvious why I just put helpers in the top-level namespace for the module rather than passing it around through multiple layers of the parser. There were a couple places in markdown where we were doing awkward `hasOwnProperty` checks for emoji-related stuff. Now we use the Python principle of ask-forgiveness-not-permission and just handle the getters returning falsy data. (It should be `undefined`, but any falsy value is unworkable in the places I changed, so I use the simpler, less brittle form.) We also break our direct dependency on `emoji_codes.json` (with some help from the prior commit). In one place I rename streamName to stream_name, fixing up an ancient naming violation that goes way back to before this code was even extracted away from echo.js. I didn't bother to split this out into a separate commit, since 2 of the 4 lines would be immediately re-modified in the subsequent commit. Note that we still depend on `fenced_code` via the global namespace, instead of simply requiring it directly or injecting it. The reason I'm postponing any action there is that we'll have to change things once we move markdown into a shared library. (The most likely outcome is that we'll rename/move both files at the same time and fix the namespace/require details as part of that commit.) Also the markdown code still relies on `_` being available in the global namespace. We aren't quite ready to share code with mobile yet, but the underscore dependency should not be problematic, since mobile already uses underscore to use the webapp's shared typing_status module.
This commit is contained in:
@@ -10,6 +10,7 @@ zrequire('user_groups');
|
||||
const emoji_codes = zrequire('emoji_codes', 'generated/emoji/emoji_codes.json');
|
||||
zrequire('emoji');
|
||||
zrequire('message_store');
|
||||
const markdown_config = zrequire('markdown_config');
|
||||
zrequire('markdown');
|
||||
|
||||
set_global('location', {
|
||||
@@ -190,7 +191,10 @@ run_test('fenced_block_defaults', () => {
|
||||
assert.equal(output, expected);
|
||||
});
|
||||
|
||||
markdown.initialize(page_params.realm_filters);
|
||||
markdown.initialize(
|
||||
page_params.realm_filters,
|
||||
markdown_config.get_helpers()
|
||||
);
|
||||
|
||||
const bugdown_data = global.read_fixture_data('markdown_test_cases.json');
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ util.is_mobile = () => false;
|
||||
global.stub_templates(() => 'some-html');
|
||||
ui.get_scroll_element = element => element;
|
||||
|
||||
zrequire('hash_util');
|
||||
zrequire('echo');
|
||||
zrequire('colorspace');
|
||||
zrequire('stream_color');
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
// Docs: https://zulip.readthedocs.io/en/latest/subsystems/markdown.html
|
||||
|
||||
// This should be initialized with a struct
|
||||
// similar to markdown_config.get_helpers().
|
||||
// See the call to markdown.initialize() in ui_init
|
||||
// for example usage.
|
||||
let helpers;
|
||||
|
||||
const realm_filter_map = new Map();
|
||||
let realm_filter_list = [];
|
||||
|
||||
@@ -58,7 +64,7 @@ exports.translate_emoticons_to_names = (text) => {
|
||||
return match;
|
||||
};
|
||||
|
||||
for (const translation of emoji.get_emoticon_translations()) {
|
||||
for (const translation of helpers.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
|
||||
@@ -123,7 +129,7 @@ exports.apply_markdown = function (message) {
|
||||
full_name = match[1];
|
||||
user_id = parseInt(match[2], 10);
|
||||
|
||||
if (!people.is_valid_full_name_and_user_id(full_name, user_id)) {
|
||||
if (!helpers.is_valid_full_name_and_user_id(full_name, user_id)) {
|
||||
user_id = undefined;
|
||||
full_name = undefined;
|
||||
}
|
||||
@@ -132,7 +138,7 @@ exports.apply_markdown = function (message) {
|
||||
if (user_id === undefined) {
|
||||
// Handle normal syntax
|
||||
full_name = mention;
|
||||
user_id = people.get_user_id_from_name(full_name);
|
||||
user_id = helpers.get_user_id_from_name(full_name);
|
||||
}
|
||||
|
||||
if (user_id === undefined) {
|
||||
@@ -147,7 +153,7 @@ exports.apply_markdown = function (message) {
|
||||
// flags on the message itself that get used by the message
|
||||
// view code and possibly our filtering code.
|
||||
|
||||
if (people.my_current_user_id() === user_id && !silently) {
|
||||
if (helpers.my_user_id() === user_id && !silently) {
|
||||
message.mentioned = true;
|
||||
message.mentioned_me_directly = true;
|
||||
}
|
||||
@@ -160,13 +166,13 @@ exports.apply_markdown = function (message) {
|
||||
|
||||
// If I mention "@aLiCe sMITH", I still want "Alice Smith" to
|
||||
// show in the pill.
|
||||
const actual_full_name = people.get_actual_name_from_user_id(user_id);
|
||||
const actual_full_name = helpers.get_actual_name_from_user_id(user_id);
|
||||
return str + _.escape(actual_full_name) + '</span>';
|
||||
},
|
||||
groupMentionHandler: function (name) {
|
||||
const group = user_groups.get_user_group_from_name(name);
|
||||
const group = helpers.get_user_group_from_name(name);
|
||||
if (group !== undefined) {
|
||||
if (user_groups.is_member_of(group.id, people.my_current_user_id())) {
|
||||
if (helpers.is_member_of_user_group(group.id, helpers.my_user_id())) {
|
||||
message.mentioned = true;
|
||||
}
|
||||
return '<span class="user-group-mention" data-user-group-id="' + group.id + '">' +
|
||||
@@ -247,12 +253,14 @@ function make_emoji_span(codepoint, title, alt_text) {
|
||||
|
||||
function handleUnicodeEmoji(unicode_emoji) {
|
||||
const codepoint = unicode_emoji.codePointAt(0).toString(16);
|
||||
const emoji_name = emoji.get_emoji_name(codepoint);
|
||||
const emoji_name = helpers.get_emoji_name(codepoint);
|
||||
|
||||
if (emoji_name) {
|
||||
const alt_text = ':' + emoji_name + ':';
|
||||
const title = emoji_name.split("_").join(" ");
|
||||
return make_emoji_span(codepoint, title, alt_text);
|
||||
}
|
||||
|
||||
return unicode_emoji;
|
||||
}
|
||||
|
||||
@@ -267,7 +275,7 @@ function handleEmoji(emoji_name) {
|
||||
// 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);
|
||||
const emoji_url = helpers.get_realm_emoji_url(emoji_name);
|
||||
|
||||
if (emoji_url) {
|
||||
return '<img alt="' + alt_text + '"' +
|
||||
@@ -275,7 +283,7 @@ function handleEmoji(emoji_name) {
|
||||
' title="' + title + '">';
|
||||
}
|
||||
|
||||
const codepoint = emoji.get_emoji_codepoint(emoji_name);
|
||||
const codepoint = helpers.get_emoji_codepoint(emoji_name);
|
||||
if (codepoint) {
|
||||
return make_emoji_span(codepoint, title, alt_text);
|
||||
}
|
||||
@@ -289,23 +297,23 @@ function handleAvatar(email) {
|
||||
' title="' + email + '">';
|
||||
}
|
||||
|
||||
function handleStream(streamName) {
|
||||
const stream = stream_data.get_sub(streamName);
|
||||
function handleStream(stream_name) {
|
||||
const stream = helpers.get_stream_by_name(stream_name);
|
||||
if (stream === undefined) {
|
||||
return;
|
||||
}
|
||||
const href = hash_util.by_stream_uri(stream.stream_id);
|
||||
const href = helpers.stream_hash(stream.stream_id);
|
||||
return '<a class="stream" data-stream-id="' + stream.stream_id + '" ' +
|
||||
'href="/' + href + '"' +
|
||||
'>' + '#' + _.escape(stream.name) + '</a>';
|
||||
}
|
||||
|
||||
function handleStreamTopic(streamName, topic) {
|
||||
const stream = stream_data.get_sub(streamName);
|
||||
function handleStreamTopic(stream_name, topic) {
|
||||
const stream = helpers.get_stream_by_name(stream_name);
|
||||
if (stream === undefined || !topic) {
|
||||
return;
|
||||
}
|
||||
const href = hash_util.by_stream_topic_uri(stream.stream_id, topic);
|
||||
const href = helpers.stream_topic_hash(stream.stream_id, topic);
|
||||
const text = '#' + _.escape(stream.name) + ' > ' + _.escape(topic);
|
||||
return '<a class="stream-topic" data-stream-id="' + stream.stream_id + '" ' +
|
||||
'href="/' + href + '"' + '>' + text + '</a>';
|
||||
@@ -416,7 +424,8 @@ exports.update_realm_filter_rules = function (realm_filters) {
|
||||
marked.InlineLexer.rules.zulip.realm_filters = marked_rules;
|
||||
};
|
||||
|
||||
exports.initialize = function (realm_filters) {
|
||||
exports.initialize = function (realm_filters, helper_config) {
|
||||
helpers = helper_config;
|
||||
|
||||
function disable_markdown_regex(rules, name) {
|
||||
rules[name] = {exec: function () {
|
||||
@@ -456,7 +465,7 @@ exports.initialize = function (realm_filters) {
|
||||
}
|
||||
|
||||
function preprocess_translate_emoticons(src) {
|
||||
if (!page_params.translate_emoticons) {
|
||||
if (!helpers.should_translate_emoticons()) {
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
47
static/js/markdown_config.js
Normal file
47
static/js/markdown_config.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This config is in a separate file for partly
|
||||
tactical reasons. We want the webapp to
|
||||
configure this one way, but we don't want to
|
||||
share this code with mobile.
|
||||
|
||||
I also wanted to make some diffs clear before
|
||||
doing any major file moves.
|
||||
|
||||
Also, I want the unit tests for markdown to
|
||||
be able to reuse this code easily (and therefore
|
||||
didn't just put this in ui_init.js).
|
||||
|
||||
Once the first steps of making markdown be a
|
||||
shared library are complete, we may tweak
|
||||
the file organization a bit.
|
||||
|
||||
Most functions here that are looking up data
|
||||
follow the convention of returning `undefined`
|
||||
when the lookups fail.
|
||||
*/
|
||||
|
||||
exports.get_helpers = () => ({
|
||||
// user stuff
|
||||
get_actual_name_from_user_id: people.get_actual_name_from_user_id,
|
||||
get_user_id_from_name: people.get_user_id_from_name,
|
||||
is_valid_full_name_and_user_id: people.is_valid_full_name_and_user_id,
|
||||
my_user_id: people.my_current_user_id,
|
||||
|
||||
// user groups
|
||||
get_user_group_from_name: user_groups.get_user_group_from_name,
|
||||
is_member_of_user_group: user_groups.is_member_of,
|
||||
|
||||
// emojis
|
||||
get_realm_emoji_url: emoji.get_realm_emoji_url,
|
||||
get_emoji_name: emoji.get_emoji_name,
|
||||
get_emoji_codepoint: emoji.get_emoji_codepoint,
|
||||
get_emoticon_translations: emoji.get_emoticon_translations,
|
||||
|
||||
// stream hashes
|
||||
get_stream_by_name: stream_data.get_sub,
|
||||
stream_hash: hash_util.by_stream_uri,
|
||||
stream_topic_hash: hash_util.by_stream_topic_uri,
|
||||
|
||||
// settings
|
||||
should_translate_emoticons: () => page_params.translate_emoticons,
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
const markdown_config = require('./markdown_config');
|
||||
|
||||
// This is where most of our initialization takes place.
|
||||
// TODO: Organize it a lot better. In particular, move bigger
|
||||
// functions to other modules.
|
||||
@@ -326,7 +328,10 @@ exports.initialize_everything = function () {
|
||||
message_fetch.initialize();
|
||||
message_scroll.initialize();
|
||||
emoji.initialize();
|
||||
markdown.initialize(page_params.realm_filters); // Must happen after emoji.initialize()
|
||||
markdown.initialize(
|
||||
page_params.realm_filters,
|
||||
markdown_config.get_helpers()
|
||||
);
|
||||
compose.initialize();
|
||||
composebox_typeahead.initialize(); // Must happen after compose.initialize()
|
||||
search.initialize();
|
||||
|
||||
Reference in New Issue
Block a user