summaryrefslogtreecommitdiffstats
path: root/comm/suite/chatzilla/xul/lib/munger.js
blob: cdbda15c540ce65ffd2c0bafc6035efb574712e8 (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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* Constructs a new munger entry, using a regexp or lambda match function, and
 * a class name (to be applied by the munger itself) or lambda replace
 * function, and the default enabled state and a start priority (used if two
 * rules match at the same index), as well as a default tag (when the munger
 * adds it based on the class name) name.
 *
 * Regular Expressions for matching should ensure that the first capturing
 * group is the one that contains the matched text. Non-capturing groups, of
 * zero-width or otherwise can be used before and after, to ensure the right
 * things are matched (e.g. to ensure whitespace before something).
 *
 * Note that for RegExp matching, the munger will search for the matched text
 * (from the first capturing group) from the leftmost point of the entire
 * match. This means that if the text that matched the first group occurs in
 * any part of the match before the group, the munger will apply to the wrong
 * bit. This is not usually a problem, but if it is you should use a
 * lambdaMatch function and be sure to return the new style return value,
 * which specifically indicates the start.
 *
 * The lambda match and lambda replace functions have this signature:
 *   lambdaMatch(text, containerTag, data, mungerEntry)
 *   lambdaReplace(text, containerTag, data, mungerEntry)
 *     - text is the entire text to find a match in/that has matched
 *     - containerTag is the element containing the text (not useful?)
 *     - data is a generic object containing properties kept throughout
 *     - mungerEntry is the CMungerEntry object for the munger itself
 *
 *   The lambdaReplace function is expected to do everything needed to put
 *   |text| into |containerTab| ready for display.
 *
 *   The return value for lambda match functions should be either:
 *     - (old style) just the text that matched
 *       (the munger will search for this text, and uses the first match)
 *     - (new style) an object with properties:
 *       - start (start index, 0 = first character)
 *       - text  (matched text)
 *       (note that |text| must start at index |start|)
 *
 *   The return value for lambda replace functions are not used.
 *
 */

function CMungerEntry(name, regex, className, priority, startPriority,
                      enable, tagName)
{
    this.name = name;
    if (name[0] != ".")
        this.description = getMsg("munger." + name, null, null);
    this.enabled = (typeof enable == "undefined" ? true : enable);
    this.enabledDefault = this.enabled;
    this.startPriority = (startPriority) ? startPriority : 0;
    this.priority = priority;
    this.tagName = (tagName) ? tagName : "html:span";

    if (isinstance(regex, RegExp))
        this.regex = regex;
    else
        this.lambdaMatch = regex;

    if (typeof className == "function")
        this.lambdaReplace = className;
    else
        this.className = className;
}

function CMunger(textMunger)
{
    this.entries = new Array();
    this.tagName = "html:span";
    this.enabled = true;
    if (textMunger)
        this.insertPlainText = textMunger;
}

CMunger.prototype.enabled = true;
CMunger.prototype.insertPlainText = insertText;

CMunger.prototype.getRule =
function mng_getrule(name)
{
    for (var p in this.entries)
    {
        if (isinstance(this.entries[p], Object))
        {
            if (name in this.entries[p])
                return this.entries[p][name];
        }
    }
    return null;
}

CMunger.prototype.addRule =
function mng_addrule(name, regex, className, priority, startPriority, enable)
{
    if (typeof this.entries[priority] != "object")
        this.entries[priority] = new Object();
    var entry = new CMungerEntry(name, regex, className, priority,
                                 startPriority, enable);
    this.entries[priority][name] = entry;
}

CMunger.prototype.delRule =
function mng_delrule(name)
{
    for (var i in this.entries)
    {
        if (typeof this.entries[i] == "object")
        {
            if (name in this.entries[i])
                delete this.entries[i][name];
        }
    }
}

CMunger.prototype.munge =
function mng_munge(text, containerTag, data)
{

    if (!containerTag)
        containerTag = document.createElementNS(XHTML_NS, this.tagName);

    // Starting from the top, for each valid priority, check all the rules,
    // return as soon as something matches.
    if (this.enabled)
    {
        for (var i = this.entries.length - 1; i >= 0; i--)
        {
            if (i in this.entries)
            {
                if (this.mungePriority(i, text, containerTag, data))
                    return containerTag;
            }
        }
    }

    // If nothing matched, we don't have to do anything,
    // just insert text (if any).
    if (text)
        this.insertPlainText(text, containerTag, data);
    return containerTag;
}

CMunger.prototype.mungePriority =
function mng_mungePriority(priority, text, containerTag, data)
{
    var matches = new Object();
    var entry;
    // Find all the matches in this priority
    for (entry in this.entries[priority])
    {
        var munger = this.entries[priority][entry];
        if (!munger.enabled)
            continue;

        var match = null;
        if (typeof munger.lambdaMatch == "function")
        {
            var rval = munger.lambdaMatch(text, containerTag, data, munger);
            if (typeof rval == "string")
                match = { start: text.indexOf(rval), text: rval };
            else if (typeof rval == "object")
                match = rval;
        }
        else
        {
            var ary = text.match(munger.regex);
            if ((ary != null) && (ary[1]))
                match = { start: text.indexOf(ary[1]), text: ary[1] };
        }

        if (match && (match.start >= 0))
        {
            match.munger = munger;
            matches[entry] = match;
        }
    }

    // Find the first matching entry...
    var firstMatch = { start: text.length, munger: null };
    var firstPriority = 0;
    for (entry in matches)
    {
        // If it matches before the existing first, or at the same spot but
        // with a higher start-priority, this is a better match.
        if (matches[entry].start < firstMatch.start ||
            ((matches[entry].start == firstMatch.start) &&
             (this.entries[priority][entry].startPriority > firstPriority)))
        {
            firstMatch = matches[entry];
            firstPriority = this.entries[priority][entry].startPriority;
        }
    }

    // Replace it.
    if (firstMatch.munger)
    {
        var munger = firstMatch.munger;
        firstMatch.end = firstMatch.start + firstMatch.text.length;

        // Need to deal with the text before the match, if there is any.
        var beforeText = text.substr(0, firstMatch.start);
        if (firstMatch.start > 0)
            this.munge(beforeText, containerTag, data);

        if (typeof munger.lambdaReplace == "function")
        {
            // The munger rule itself should take care of munging the 'inside'
            // of the match.
            munger.lambdaReplace(firstMatch.text, containerTag, data, munger);
            this.munge(text.substr(firstMatch.end), containerTag, data);

            return containerTag;
        }
        else
        {
            var tag = document.createElementNS(XHTML_NS, munger.tagName);
            tag.setAttribute("class", munger.className + calcClass(data));

            // Don't let this rule match again when we recurse.
            munger.enabled = false;
            this.munge(firstMatch.text, tag, data);
            munger.enabled = true;

            containerTag.appendChild(tag);

            this.munge(text.substr(firstMatch.end), containerTag, data);

            return containerTag;
        }
    }
    return null;
}

function insertText(text, containerTag, data)
{
    var textNode = document.createTextNode(text);
    containerTag.appendChild(textNode);
}