From 144cb58a4774a789e9b6ca2ec80b15f6dbd170d0 Mon Sep 17 00:00:00 2001
From: Daniel Gasienica <daniel@signal.org>
Date: Tue, 10 Apr 2018 13:03:05 -0400
Subject: [PATCH] Add `HTML` module for rendering messages

---
 ts/html/index.ts                | 12 +++++
 ts/test-unit/html/index_test.ts | 96 +++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+)
 create mode 100644 ts/html/index.ts
 create mode 100644 ts/test-unit/html/index_test.ts

diff --git a/ts/html/index.ts b/ts/html/index.ts
new file mode 100644
index 000000000..b3c6ae211
--- /dev/null
+++ b/ts/html/index.ts
@@ -0,0 +1,12 @@
+import linkTextInternal from 'link-text';
+
+
+export const linkText = (value: string): string =>
+  linkTextInternal(value, { target: '_blank' });
+
+export const replaceLineBreaks = (value: string): string =>
+  value.replace(/\r?\n/g, '<br>');
+
+// NOTE: How can we use `lodash/fp` `compose` with type checking?
+export const render = (value: string): string =>
+  replaceLineBreaks(linkText(value));
diff --git a/ts/test-unit/html/index_test.ts b/ts/test-unit/html/index_test.ts
new file mode 100644
index 000000000..f70912b78
--- /dev/null
+++ b/ts/test-unit/html/index_test.ts
@@ -0,0 +1,96 @@
+import 'mocha';
+import { assert } from 'chai';
+
+import * as HTML from '../../html';
+
+interface Test {
+  input: string;
+  name: string;
+  output?: string;
+  outputHref?: string;
+  outputLabel?: string;
+  postText?: string;
+  preText?: string;
+  skipped?: boolean;
+}
+
+describe('HTML', () => {
+  describe('linkText', () => {
+    const TESTS: Array<Test> = [
+      {
+        name: 'square brackets',
+        input: 'https://www.example.com/test.html?foo=bar&baz[qux]=quux',
+        output: 'https://www.example.com/test.html?foo=bar&amp;baz[qux]=quux',
+      },
+      {
+        name: 'Chinese characters',
+        input: 'https://zh.wikipedia.org/zh-hans/信号',
+        output: 'https://zh.wikipedia.org/zh-hans/信号',
+      },
+      {
+        name: 'Cyrillic characters',
+        input: 'https://ru.wikipedia.org/wiki/Сигнал',
+        output: 'https://ru.wikipedia.org/wiki/Сигнал',
+      },
+      {
+        skipped: true,
+        name: 'trailing exclamation points',
+        input: 'https://en.wikipedia.org/wiki/Mother!',
+        output: 'https://en.wikipedia.org/wiki/Mother!',
+      },
+      {
+        name: 'single quotes',
+        input: "https://www.example.com/this-couldn't-be-true",
+        output: "https://www.example.com/this-couldn#39;t-be-true",
+      },
+      {
+        name: 'special characters before URL begins',
+        preText: 'wink ;)',
+        input: 'https://www.youtube.com/watch?v=oHg5SJYRHA0',
+        output: 'https://www.youtube.com/watch?v=oHg5SJYRHA0',
+      },
+      {
+        name: 'URLs without protocols',
+        input: 'github.com',
+        outputHref: 'http://github.com',
+        outputLabel: 'github.com',
+      },
+    ];
+
+    TESTS.forEach((test) => {
+      (test.skipped ? it.skip : it)(`should handle ${test.name}`, () => {
+        const preText = test.preText || 'Hello ';
+        const postText = test.postText || ' World!';
+        const input: string = `${preText}${test.input}${postText}`;
+        const expected: string = [
+          preText,
+          `<a href="${test.outputHref || test.output}" target="_blank">`,
+          test.outputLabel || test.output,
+          '</a>',
+          postText,
+        ].join('');
+
+        const actual = HTML.linkText(input);
+        assert.equal(actual, expected);
+      });
+    });
+  });
+
+  describe('render', () => {
+    it('should preserve line breaks', () => {
+      const input: string = 'Hello\n\n\nWorld!';
+      const expected: string = 'Hello<br><br><br>World!';
+
+      const actual = HTML.render(input);
+      assert.equal(actual, expected);
+    });
+
+    it('should escape HTML', () => {
+      const input: string = "Hello\n<script>alert('evil');</script>World!";
+      const expected: string = 'Hello<br>&lt;script&gt;alert(&#39;evil&#39;);&lt;/script&gt;World!';
+
+      const actual = HTML.render(input);
+      assert.equal(actual, expected);
+    });
+  });
+});