• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

siddharthvp / mwn / 13882525620

16 Mar 2025 10:35AM UTC coverage: 83.084% (-0.6%) from 83.662%
13882525620

push

github

siddharthvp
ci: Run CI for release/ branches as well

663 of 858 branches covered (77.27%)

Branch coverage included in aggregate %.

1169 of 1347 relevant lines covered (86.79%)

131.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.51
/src/static_utils.ts
1
/**
2
 * Static functions on mwn
3
 */
4

5
import type { MwnTitle } from './bot';
6

7
/**
8
 * Get wikitext for a new link
9
 * @param target
10
 * @param [displaytext]
11
 */
12
export function link(target: string | MwnTitle, displaytext?: string): string {
1✔
13
        if (typeof target === 'string') {
6✔
14
                return '[[' + target + (displaytext ? '|' + displaytext : '') + ']]';
2✔
15
        }
16
        return (
4✔
17
                '[[' +
18
                target.toText() +
19
                (target.fragment ? '#' + target.fragment : '') +
4✔
20
                (displaytext ? '|' + displaytext : '') +
4✔
21
                ']]'
22
        );
23
}
24

25
/**
26
 * Get wikitext for a template usage
27
 * @param title
28
 * @param [parameters={}] - template parameters as object
29
 */
30
export function template(title: string | MwnTitle, parameters: { [parameter: string]: string } = {}): string {
1!
31
        if (typeof title !== 'string') {
3✔
32
                // title object provided
33
                if (title.namespace === 10) {
2✔
34
                        title = title.getMainText(); // skip namespace name for templates
1✔
35
                } else if (title.namespace === 0) {
1!
36
                        title = ':' + title.toText(); // prefix colon for mainspace
1✔
37
                } else {
38
                        title = title.toText();
×
39
                }
40
        }
41
        return (
3✔
42
                '{{' +
43
                title +
44
                Object.entries(parameters)
45
                        .filter(([, value]) => !!value) // ignore params with no value
12✔
46
                        .map(([name, value]) => `|${name}=${value}`)
12✔
47
                        .join('') +
48
                '}}'
49
        );
50
}
51

52
export class Table {
1✔
53
        text: string;
54
        multiline: boolean;
55
        numRows = 0;
11✔
56

57
        /**
58
         * @param {Object} [config={}]
59
         * @config {boolean} plain - plain table without borders (default: false)
60
         * @config {boolean} sortable - make columns sortable (default: true)
61
         * @config {string} style - style attribute
62
         * @config {boolean} multiline - put each cell of the table on a new line,
63
         * this causes no visual changes, but the wikitext representation is different.
64
         * This is more reliable. (default: true)
65
         */
66
        constructor(
67
                config: {
68
                        plain?: boolean;
69
                        sortable?: boolean;
70
                        style?: string;
71
                        multiline?: boolean;
72
                        classes?: string[];
73
                } = {}
3✔
74
        ) {
75
                let classes = new Set(config.classes);
11✔
76
                if (!config.plain) {
11✔
77
                        classes.add('wikitable');
7✔
78
                }
79
                if (config.sortable !== false) {
11✔
80
                        classes.add('sortable');
8✔
81
                }
82
                if (config.multiline !== false) {
11✔
83
                        this.multiline = true;
8✔
84
                }
85
                this.text = `{|`;
11✔
86
                if (classes.size) {
11✔
87
                        this.text += ` class="${[...classes].join(' ')}"`;
9✔
88
                }
89
                if (config.style) {
11✔
90
                        this.text += ` style="${config.style}"`;
2✔
91
                }
92
                this.text += '\n';
11✔
93
        }
94

95
        _makecell(cell: string | { [attribute: string]: string }, isHeader?: boolean): string {
96
                // typeof null is also object!
97
                if (cell && typeof cell === 'object') {
45✔
98
                        let text = isHeader ? `scope="col"` : ``;
5✔
99
                        for (let [key, value] of Object.entries(cell)) {
5✔
100
                                if (key === 'label') {
9✔
101
                                        continue;
5✔
102
                                }
103
                                text += ` ${key}="${value}"`;
4✔
104
                        }
105
                        text += ` | ${cell.label}`;
5✔
106
                        return text;
5✔
107
                }
108
                return String(cell);
40✔
109
        }
110
        /**
111
         * Add the headers
112
         * @param headers - array of header items
113
         */
114
        addHeaders(headers: (string | { [attribute: string]: string })[]): void {
115
                this.text += `|-\n`; // row separator
5✔
116
                if (this.multiline) {
5✔
117
                        this.text += headers.map((e) => `! ${this._makecell(e, true)} \n`).join('');
6✔
118
                } else {
119
                        this.text += `! ` + headers.map((e) => this._makecell(e, true)).join(' !! ') + '\n';
9✔
120
                }
121
        }
122

123
        /**
124
         * Add a row to the table
125
         * @param fields - array of items on the row,
126
         * @param attributes - row attributes
127
         */
128
        addRow(fields: string[], attributes: { [attribute: string]: string } = {}): void {
9✔
129
                this.numRows++;
10✔
130
                let attributetext = '';
10✔
131
                Object.entries(attributes).forEach(([key, value]) => {
10✔
132
                        attributetext += ` ${key}="${value}"`;
1✔
133
                });
134
                this.text += `|-${attributetext}\n`; // row separator
10✔
135
                if (this.multiline) {
10✔
136
                        this.text += fields.map((e) => `| ${this._makecell(e)} \n`).join('');
12✔
137
                } else {
138
                        this.text += `| ` + fields.map((f) => this._makecell(f)).join(' || ') + '\n';
18✔
139
                }
140
        }
141

142
        getNumRows() {
143
                return this.numRows;
×
144
        }
145

146
        /** Returns the final table wikitext */
147
        getText(): string {
148
                return this.text + `|}`; // add the table closing tag and return
11✔
149
        }
150
}
151

152
/**
153
 * Encode the string like PHP's rawurlencode
154
 *
155
 * @param {string} str String to be encoded.
156
 * @return {string} Encoded string
157
 */
158
function rawurlencode(str: string): string {
159
        return encodeURIComponent(String(str))
12✔
160
                .replace(/!/g, '%21')
161
                .replace(/'/g, '%27')
162
                .replace(/\(/g, '%28')
163
                .replace(/\)/g, '%29')
164
                .replace(/\*/g, '%2A')
165
                .replace(/~/g, '%7E');
166
}
167

168
/**
169
 * Check if string is an IPv4 address
170
 * @param {string} address
171
 * @param {boolean} [allowBlock=false]
172
 * @return {boolean}
173
 */
174
function isIPv4Address(address: string, allowBlock?: boolean): boolean {
175
        let block,
176
                RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])',
22✔
177
                RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
22✔
178
        if (typeof address !== 'string') {
22✔
179
                return false;
4✔
180
        }
181
        block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
18!
182
        return new RegExp('^' + RE_IP_ADD + block + '$').test(address);
18✔
183
}
184

185
/**
186
 * Check if the string is an IPv6 address
187
 * @param {string} address
188
 * @param {boolean} [allowBlock=false]
189
 * @return {boolean}
190
 */
191
function isIPv6Address(address: string, allowBlock?: boolean): boolean {
192
        let block, RE_IPV6_ADD;
193
        if (typeof address !== 'string') {
80!
194
                return false;
×
195
        }
196
        block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
80!
197
        RE_IPV6_ADD =
80✔
198
                '(?:' + // starts with "::" (including "::")
199
                ':(?::|(?::' +
200
                '[0-9A-Fa-f]{1,4}' +
201
                '){1,7})' +
202
                '|' + // ends with "::" (except "::")
203
                '[0-9A-Fa-f]{1,4}' +
204
                '(?::' +
205
                '[0-9A-Fa-f]{1,4}' +
206
                '){0,6}::' +
207
                '|' + // contains no "::"
208
                '[0-9A-Fa-f]{1,4}' +
209
                '(?::' +
210
                '[0-9A-Fa-f]{1,4}' +
211
                '){7}' +
212
                ')';
213
        if (new RegExp('^' + RE_IPV6_ADD + block + '$').test(address)) {
80✔
214
                return true;
32✔
215
        }
216
        // contains one "::" in the middle (single '::' check below)
217
        RE_IPV6_ADD = '[0-9A-Fa-f]{1,4}' + '(?:::?' + '[0-9A-Fa-f]{1,4}' + '){1,6}';
48✔
218
        return new RegExp('^' + RE_IPV6_ADD + block + '$').test(address) && /::/.test(address) && !/::.*::/.test(address);
48✔
219
}
220

221
/**
222
 * Escape string for safe inclusion in regular expression.
223
 * The following characters are escaped:
224
 *     \ { } ( ) | . ? * + - ^ $ [ ]
225
 * @param {string} str String to escape
226
 * @return {string} Escaped string
227
 */
228
function escapeRegExp(str: string): string {
229
        // eslint-disable-next-line no-useless-escape
230
        return str.replace(/([\\{}()|.?*+\-^$\[\]])/g, '\\$1');
16✔
231
}
232

233
/**
234
 * Escape a string for HTML. Converts special characters to HTML entities.
235
 *
236
 *     Util.escapeHtml( '< > \' & "' );
237
 *     // Returns &lt; &gt; &#039; &amp; &quot;
238
 *
239
 * @param {string} s - The string to escape
240
 * @return {string} HTML
241
 */
242
function escapeHtml(s: string): string {
243
        return s.replace(/['"<>&]/g, function escapeCallback(s) {
2✔
244
                switch (s) {
6✔
245
                        case "'":
6!
246
                                return '&#039;';
2✔
247
                        case '"':
248
                                return '&quot;';
2✔
249
                        case '<':
250
                                return '&lt;';
1✔
251
                        case '>':
252
                                return '&gt;';
1✔
253
                        case '&':
254
                                return '&amp;';
×
255
                }
256
        });
257
}
258

259
/**
260
 * Encode page titles for use in a URL like mw.util.wikiUrlencode()
261
 *
262
 * We want / and : to be included as literal characters in our title URLs
263
 * as they otherwise fatally break the title. The others are decoded because
264
 * we can, it's prettier and matches behaviour of `wfUrlencode` in PHP.
265
 *
266
 * @param {string} str String to be encoded.
267
 * @return {string} Encoded string
268
 */
269
function wikiUrlencode(str: string): string {
270
        return (
11✔
271
                rawurlencode(str)
272
                        .replace(/%20/g, '_')
273
                        // wfUrlencode replacements
274
                        .replace(/%3B/g, ';')
275
                        .replace(/%40/g, '@')
276
                        .replace(/%24/g, '$')
277
                        .replace(/%21/g, '!')
278
                        .replace(/%2A/g, '*')
279
                        .replace(/%28/g, '(')
280
                        .replace(/%29/g, ')')
281
                        .replace(/%2C/g, ',')
282
                        .replace(/%2F/g, '/')
283
                        .replace(/%7E/g, '~')
284
                        .replace(/%3A/g, ':')
285
        );
286
}
287

288
/**
289
 * Check whether a string is an IP address
290
 * @param {string} address String to check
291
 * @param {boolean} [allowBlock=false] True if a block of IPs should be allowed
292
 * @return {boolean}
293
 */
294
function isIPAddress(address: string, allowBlock?: boolean): boolean {
295
        return isIPv4Address(address, allowBlock) || isIPv6Address(address, allowBlock);
×
296
}
297

298
export const util = {
1✔
299
        escapeRegExp,
300
        escapeHtml,
301
        rawurlencode,
302
        wikiUrlencode,
303
        isIPv4Address,
304
        isIPv6Address,
305
        isIPAddress,
306
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc