markdown: Move translate_emoticons_to_names().

Most of this logic is specific to markdown
message processing, so we move the code to
markdown.js.

The only responsibility that we leave with
`emoji.js` is to provide us with a list
of translations (regex and replacement text).

But now `markdown.js` actually (directly) executes
those translations against Zulip messages
as part of its preprocessing.

This should simplify the upcoming mobile conversion.
Instead of mobile needing to duplicate this fairly
complex function, they will just need to pass
us in a list similar to `emoji_translations` inside
of `emoji.js`.  That code has a comment that shows
what the data structure looks like.
This commit is contained in:
Steve Howell 2020-02-15 14:21:32 +00:00 committed by Tim Abbott
parent 6050a5bdd6
commit 5e9df7a0a6
4 changed files with 77 additions and 76 deletions

View File

@ -6,7 +6,6 @@ set_global('page_params', {
set_global('upload_widget', {});
set_global('blueslip', global.make_zblueslip());
const emoji_codes = zrequire('emoji_codes', 'generated/emoji/emoji_codes.json');
zrequire('emoji');
zrequire('markdown');
@ -81,39 +80,3 @@ run_test('get_canonical_name', () => {
assert.equal(blueslip.get_test_logs('error').length, 1);
blueslip.clear_test_data();
});
run_test('translate_emoticons_to_names', () => {
// Simple test
const test_text = 'Testing :)';
const expected = 'Testing :slight_smile:';
const result = emoji.translate_emoticons_to_names(test_text);
assert.equal(expected, result);
// Extensive tests.
// The following code loops over the test cases and each emoticon conversion
// to generate multiple test cases.
const testcases = [
{name: 'only emoticon', original: '<original>', expected: '<converted>'},
{name: 'space at start', original: ' <original>', expected: ' <converted>'},
{name: 'space at end', original: '<original> ', expected: '<converted> '},
{name: 'symbol at end', original: '<original>!', expected: '<converted>!'},
{name: 'symbol at start', original: 'Hello,<original>', expected: 'Hello,<converted>'},
{name: 'after a word', original: 'Hello<original>', expected: 'Hello<original>'},
{name: 'between words', original: 'Hello<original>World', expected: 'Hello<original>World'},
{name: 'end of sentence', original: 'End of sentence. <original>', expected: 'End of sentence. <converted>'},
{name: 'between symbols', original: 'Hello.<original>! World.', expected: 'Hello.<original>! World.'},
{name: 'before end of sentence', original: 'Hello <original>!', expected: 'Hello <converted>!'},
];
for (const [shortcut, full_name] of Object.entries(emoji_codes.emoticon_conversions)) {
for (const t of testcases) {
const converted_value = full_name;
let original = t.original;
let expected = t.expected;
original = original.replace(/(<original>)/g, shortcut);
expected = expected.replace(/(<original>)/g, shortcut)
.replace(/(<converted>)/g, converted_value);
const result = emoji.translate_emoticons_to_names(original);
assert.equal(result, expected);
}
}
});

View File

@ -608,3 +608,39 @@ run_test('misc_helpers', () => {
markdown.set_name_in_mention_element(elem, 'Aaron, but silent');
assert.equal(elem.text(), 'Aaron, but silent');
});
run_test('translate_emoticons_to_names', () => {
// Simple test
const test_text = 'Testing :)';
const expected = 'Testing :slight_smile:';
const result = markdown.translate_emoticons_to_names(test_text);
assert.equal(expected, result);
// Extensive tests.
// The following code loops over the test cases and each emoticon conversion
// to generate multiple test cases.
const testcases = [
{name: 'only emoticon', original: '<original>', expected: '<converted>'},
{name: 'space at start', original: ' <original>', expected: ' <converted>'},
{name: 'space at end', original: '<original> ', expected: '<converted> '},
{name: 'symbol at end', original: '<original>!', expected: '<converted>!'},
{name: 'symbol at start', original: 'Hello,<original>', expected: 'Hello,<converted>'},
{name: 'after a word', original: 'Hello<original>', expected: 'Hello<original>'},
{name: 'between words', original: 'Hello<original>World', expected: 'Hello<original>World'},
{name: 'end of sentence', original: 'End of sentence. <original>', expected: 'End of sentence. <converted>'},
{name: 'between symbols', original: 'Hello.<original>! World.', expected: 'Hello.<original>! World.'},
{name: 'before end of sentence', original: 'Hello <original>!', expected: 'Hello <converted>!'},
];
for (const [shortcut, full_name] of Object.entries(emoji_codes.emoticon_conversions)) {
for (const t of testcases) {
const converted_value = full_name;
let original = t.original;
let expected = t.expected;
original = original.replace(/(<original>)/g, shortcut);
expected = expected.replace(/(<original>)/g, shortcut)
.replace(/(<converted>)/g, converted_value);
const result = markdown.translate_emoticons_to_names(original);
assert.equal(result, expected);
}
}
});

View File

@ -184,43 +184,6 @@ exports.get_canonical_name = function (emoji_name) {
return emoji_codes.codepoint_to_name[codepoint];
};
// Translates emoticons in a string to their colon syntax.
exports.translate_emoticons_to_names = function translate_emoticons_to_names(text) {
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 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;
};
exports.get_emoticon_translations = () => emoticon_translations;
window.emoji = exports;

View File

@ -34,6 +34,45 @@ exports.set_name_in_mention_element = function (element, name) {
}
};
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;
};
exports.contains_backend_only_syntax = function (content) {
// Try to guess whether or not a message has bugdown in it
// If it doesn't, we can immediately render it client-side
@ -376,7 +415,7 @@ exports.initialize = function () {
// In this scenario, the message has to be from the user, so the only
// requirement should be that they have the setting on.
return emoji.translate_emoticons_to_names(src);
return exports.translate_emoticons_to_names(src);
}
// Disable lheadings