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, '
');
+
+// 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 = [
+ {
+ name: 'square brackets',
+ input: 'https://www.example.com/test.html?foo=bar&baz[qux]=quux',
+ output: 'https://www.example.com/test.html?foo=bar&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,
+ ``,
+ test.outputLabel || test.output,
+ '',
+ 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
World!';
+
+ const actual = HTML.render(input);
+ assert.equal(actual, expected);
+ });
+
+ it('should escape HTML', () => {
+ const input: string = "Hello\nWorld!";
+ const expected: string = 'Hello
<script>alert('evil');</script>World!';
+
+ const actual = HTML.render(input);
+ assert.equal(actual, expected);
+ });
+ });
+});