diff --git a/frontend_tests/zjsunit/bugdown_assert.js b/frontend_tests/zjsunit/bugdown_assert.js index 66253daae8..ee9357fb44 100644 --- a/frontend_tests/zjsunit/bugdown_assert.js +++ b/frontend_tests/zjsunit/bugdown_assert.js @@ -23,6 +23,8 @@ const jsdom = require('jsdom'); const _ = require('underscore'); +const mdiff = require('./mdiff.js'); + // Module-level global instance of MarkdownComparer, initialized when needed let _markdownComparerInstance = null; @@ -155,25 +157,28 @@ class MarkdownComparer { } } +function returnComparer() { + if (!_markdownComparerInstance) { + _markdownComparerInstance = new MarkdownComparer((actual, expected) => { + return [ + "Actual and expected output do not match. Showing diff", + mdiff.diff_strings(actual, expected), + ].join('\n'); + }); + } + return _markdownComparerInstance; +} + module.exports = { equal(expected, actual, message) { - if (!_markdownComparerInstance) { - _markdownComparerInstance = new MarkdownComparer(); - } - _markdownComparerInstance.assertEqual(actual, expected, message); + returnComparer().assertEqual(actual, expected, message); }, notEqual(expected, actual, message) { - if (!_markdownComparerInstance) { - _markdownComparerInstance = new MarkdownComparer(); - } - _markdownComparerInstance.assertNotEqual(actual, expected, message); + returnComparer().assertNotEqual(actual, expected, message); }, setFormatter(output_formatter) { - if (!_markdownComparerInstance) { - _markdownComparerInstance = new MarkdownComparer(); - } - _markdownComparerInstance.setFormatter(output_formatter); + returnComparer().setFormatter(output_formatter); }, }; diff --git a/frontend_tests/zjsunit/mdiff.js b/frontend_tests/zjsunit/mdiff.js new file mode 100644 index 0000000000..3e7f80cbe3 --- /dev/null +++ b/frontend_tests/zjsunit/mdiff.js @@ -0,0 +1,146 @@ +/** + * mdiff.js + * + * Used to produce colorful and informative diffs for comparison of generated + * Markdown. Unlike the built-in diffs used in python or node.js assert libraries, + * is actually designed to be effective for long, single-line comparisons. + * + * Based on diffing library difflib, a js port of the python library. + * + * The sole exported function diff_strings(string_0, string_1) returns a pretty-printed + * unicode string containing their diff. + */ + +const _ = require('underscore'); +const difflib = require('difflib'); + +function apply_color(input_string, changes) { + let previous_index = 0; + let processed_string = input_string.slice(0,2); + input_string = input_string.slice(2); + + const formatter = { + delete : (string) => { return "\u001b[31m" + string + "\u001b[0m"; }, + insert : (string) => { return "\u001b[32m" + string + "\u001b[0m"; }, + replace : (string) => { return "\u001b[33m" + string + "\u001b[0m"; }, + }; + changes.forEach((change) => { + if (formatter.hasOwnProperty(change.tag)) { + processed_string += input_string.slice(previous_index, change.beginning_index); + processed_string += formatter[change.tag]( + input_string.slice(change.beginning_index, change.ending_index) + ); + previous_index = change.ending_index; + } + }); + + processed_string += input_string.slice(previous_index); + return processed_string; +} + +/** + * The library difflib produces diffs that look as follows: + * + * -
upgrade! yes
+ * ? ^^ - + * +downgrade yes.
+ * ? ^^^^ + + * + * The purpose of this function is to facilitate converting these diffs into + * colored versions, where the question-mark lines are removed, replaced with + * directions to add appropriate color to the lines that they annotate. + */ +function parse_questionmark_line(questionmark_line) { + let current_sequence = ""; // Either "^", "-", "+", or "" + let beginning_index = 0; + let index = 0; + + const changes_list = []; + const aliases = { + "^" : "replace", + "+" : "insert", + "-" : "delete", + }; + const add_change = () => { + if (current_sequence) { + changes_list.push({ + tag : aliases[current_sequence], + beginning_index, + ending_index : index, + }); + current_sequence = ""; + } + }; + + questionmark_line = questionmark_line.slice(2).trimRight("\n"); + + for (const character of questionmark_line) { + if (aliases.hasOwnProperty(character)) { + if (current_sequence !== character) { + add_change(); + current_sequence = character; + beginning_index = index; + } + } else { + add_change(); + } + index += 1; + } + + // In case we have a "change" involving the last character on a line + // e.g. a string such as "? ^^ -- ++++" + add_change(); + + return changes_list; +} + +function diff_strings(string_0, string_1) { + let output_lines = []; + let ndiff_output = ""; + let changes_list = []; + + ndiff_output = difflib.ndiff(string_0.split("\n"), string_1.split("\n")); + + ndiff_output.forEach((line) => { + if (line.startsWith("+")) { + output_lines.push(line); + } else if (line.startsWith("-")) { + output_lines.push(line); + } else if (line.startsWith("?")) { + changes_list = parse_questionmark_line(line); + output_lines[output_lines.length - 1] = apply_color( + output_lines[output_lines.length -1], changes_list); + } else { + output_lines.push(line); + } + }); + + const emphasize_codes = (string) => { + return "\u001b[34m" + string.slice(0,1) + "\u001b[0m" + string.slice(1); + }; + output_lines = _.map(output_lines, emphasize_codes); + + return output_lines.join("\n"); +} + +module.exports = { diff_strings }; + +// Simple CLI for this module +// Only run this code if called as a command-line utility +if (require.main === module) { + // First two args are just "node" and "mdiff.js" + const argv = require('minimist')(process.argv.slice(2)); + + if (_.has(argv, "help")) { + console.log(process.argv[0] + " " + process.argv[1] + + " [ --help ]" + + " string_0" + + " string_1" + + "\n" + + "Where string_0 and string_1 are the strings to be diffed" + ); + } + + const output = diff_strings(argv._[0], argv._[1]); + console.log(output); +} diff --git a/package.json b/package.json index 59a830b471..ffc57a2041 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@types/webpack": "3.0.13", "blueimp-md5": "2.10.0", "clipboard": "1.5.16", + "difflib": "0.2.4", "emoji-datasource": "3.0.0", "emoji-datasource-apple": "3.0.0", "emoji-datasource-emojione": "3.0.0", diff --git a/version.py b/version.py index ce57d9beb4..054b789c9c 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ ZULIP_VERSION = "1.7.1+git" -PROVISION_VERSION = '14.1' +PROVISION_VERSION = '14.2' diff --git a/yarn.lock b/yarn.lock index 48b916f25d..9d311c541e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1207,6 +1207,12 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +difflib@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/difflib/-/difflib-0.2.4.tgz#b5e30361a6db023176d562892db85940a718f47e" + dependencies: + heap ">= 0.2.0" + doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -2631,6 +2637,10 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +"heap@>= 0.2.0": + version "0.2.6" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"