summaryrefslogtreecommitdiffstats
path: root/web/react/utils/text_formatting.jsx
blob: 930a6bbfbb1d795963fed0ec156bb4b4a976f84f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.

const Autolinker = require('autolinker');
const Constants = require('./constants.jsx');
const UserStore = require('../stores/user_store.jsx');

export function formatText(text, options = {}) {
    let output = sanitize(text);
    let tokens = new Map();

    output = stripLinks(output, tokens);
    output = stripAtMentions(output, tokens);
    output = stripSelfMentions(output, tokens);
    output = stripHashtags(output, tokens);

    output = replaceTokens(output, tokens);

    output = replaceNewlines(output, options.singleline);

    return output;

    // TODO highlight search terms

    // TODO leave space for markdown
}

export function sanitize(text) {
    let output = text;

    // normal string.replace only does a single occurrance so use a regex instead
    output = output.replace(/&/g, '&');
    output = output.replace(/</g, '&lt;');
    output = output.replace(/>/g, '&gt;');

    return output;
}

function stripLinks(text, tokens) {
    function stripLink(autolinker, match) {
        let text = match.getMatchedText();
        let url = text;
        if (!url.startsWith('http')) {
            url = `http://${text}`;
        }

        const index = tokens.size;
        const alias = `LINK${index}`;

        tokens.set(alias, {
            value: `<a class='theme' target='_blank' href='${url}'>${text}</a>`,
            originalText: text
        });

        return alias;
    }

    // we can't just use a static autolinker because we need to set replaceFn
    const autolinker = new Autolinker({
        urls: true,
        email: true,
        phone: false,
        twitter: false,
        hashtag: false,
        replaceFn: stripLink
    });

    return autolinker.link(text);
}

function stripAtMentions(text, tokens) {
    let output = text;

    function stripAtMention(fullMatch, prefix, mention, username) {
        const usernameLower = username.toLowerCase();
        if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) {
            const index = tokens.size;
            const alias = `ATMENTION${index}`;

            tokens.set(alias, {
                value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`,
                originalText: mention
            });

            return prefix + alias;
        } else {
            return fullMatch;
        }
    }

    output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, stripAtMention);

    return output;
}
window.stripAtMentions = stripAtMentions;

function stripSelfMentions(text, tokens) {
    let output = text;

    let mentionKeys = UserStore.getCurrentMentionKeys();

    // look for any existing tokens which are self mentions and should be highlighted
    var newTokens = new Map();
    for (let [alias, token] of tokens) {
        if (mentionKeys.indexOf(token.originalText) !== -1) {
            const index = newTokens.size;
            const newAlias = `SELFMENTION${index}`;

            newTokens.set(newAlias, {
                value: `<span class='mention-highlight'>${alias}</span>`,
                originalText: token.originalText
            });

            output = output.replace(alias, newAlias);
        }
    }

    // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
    for (let newToken of newTokens) {
        tokens.set(newToken[0], newToken[1]);
    }

    // look for self mentions in the text
    function stripSelfMention(fullMatch, prefix, mention) {
        const index = tokens.size;
        const alias = `SELFMENTION${index}`;

        tokens.set(alias, {
            value: `<span class='mention-highlight'>${mention}</span>`,
            originalText: mention
        });

        return prefix + alias;
    }

    for (let mention of UserStore.getCurrentMentionKeys()) {
        output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), stripSelfMention);
    }

    return output;
}

function stripHashtags(text, tokens) {
    let output = text;

    var newTokens = new Map();
    for (let [alias, token] of tokens) {
        if (token.originalText.startsWith('#')) {
            const index = newTokens.size;
            const newAlias = `HASHTAG${index}`;

            newTokens.set(newAlias, {
                value: `<a class='mention-link' href='#' data-mention='${token.originalText}'>${token.originalText}</a>`,
                originalText: token.originalText
            });

            output = output.replace(alias, newAlias);
        }
    }

    // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
    for (let newToken of newTokens) {
        tokens.set(newToken[0], newToken[1]);
    }

    // look for hashtags in the text
    function stripHashtag(fullMatch, prefix, hashtag) {
        const index = tokens.size;
        const alias = `HASHTAG${index}`;

        tokens.set(alias, {
            value: `<a class='mention-link' href='#' data-mention='${hashtag}'>${hashtag}</a>`,
            originalText: hashtag
        });

        return prefix + alias;
    }

    output = output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, stripHashtag);

    return output;
}

function replaceTokens(text, tokens) {
    let output = text;

    // iterate backwards through the map so that we do replacement in the opposite order that we added tokens
    const aliases = [...tokens.keys()];
    for (let i = aliases.length - 1; i >= 0; i--) {
        const alias = aliases[i];
        const token = tokens.get(alias);
        console.log('replacing ' + alias + ' with ' + token.value);
        output = output.replace(alias, token.value);
    }

    return output;
}
window.replaceTokens = replaceTokens;

function replaceNewlines(text, singleline) {
    if (!singleline) {
        return text.replace(/\n/g, '<br />');
    } else {
        return text.replace(/\n/g, ' ');
    }
}