ya-bbcode 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ya-bbcode.js DELETED
@@ -1,355 +0,0 @@
1
- 'use strict';
2
-
3
- class yabbcode {
4
- tags = {
5
- 'url': {
6
- type: 'replace',
7
- open: attr => `<a href="${attr || '#'}">`,
8
- close: '</a>',
9
- },
10
- 'quote': {
11
- type: 'replace',
12
- open: attr => `<blockquote author="${attr || ''}">`,
13
- close: '</blockquote>',
14
- },
15
- 'b': {
16
- type: 'replace',
17
- open: '<strong>',
18
- close: '</strong>',
19
- },
20
- 'u': {
21
- type: 'replace',
22
- open: '<u>',
23
- close: '</u>',
24
- },
25
- 'i': {
26
- type: 'replace',
27
- open: '<i>',
28
- close: '</i>',
29
- },
30
- 'h1': {
31
- type: 'replace',
32
- open: '<h1>',
33
- close: '</h1>',
34
- },
35
- 'h2': {
36
- type: 'replace',
37
- open: '<h2>',
38
- close: '</h2>',
39
- },
40
- 'h3': {
41
- type: 'replace',
42
- open: '<h3>',
43
- close: '</h3>',
44
- },
45
- 'h4': {
46
- type: 'replace',
47
- open: '<h4>',
48
- close: '</h4>',
49
- },
50
- 'h5': {
51
- type: 'replace',
52
- open: '<h5>',
53
- close: '</h5>',
54
- },
55
- 'h6': {
56
- type: 'replace',
57
- open: '<h6>',
58
- close: '</h6>',
59
- },
60
- 'code': {
61
- type: 'replace',
62
- open: '<code>',
63
- close: '</code>',
64
- },
65
- 'strike': {
66
- type: 'replace',
67
- open: '<span class="yabbcode-strike">',
68
- close: '</span>',
69
- },
70
- 'spoiler': {
71
- type: 'replace',
72
- open: '<span class="yabbcode-spoiler">',
73
- close: '</span>',
74
- },
75
- 'list': {
76
- type: 'replace',
77
- open: '<ul>',
78
- close: '</ul>',
79
- },
80
- 'olist': {
81
- type: 'replace',
82
- open: '<ol>',
83
- close: '</ol>',
84
- },
85
- '*': {
86
- type: 'replace',
87
- open: '<li>',
88
- close: null,
89
- },
90
- 'img': {
91
- type: 'content',
92
- replace: (attr, content) => {
93
- if(!content) {
94
- return '';
95
- }
96
- return `<img src="${content}" alt="${attr || ''}"/>`;
97
- },
98
- },
99
- 'noparse': {
100
- type: 'ignore',
101
- },
102
- };
103
- regex = {
104
- tags: /(\[[^\]^]+])/g,
105
- newline: /\r\n|\r|\n/g,
106
- placeholders: /\[TAG-[1-9]+]/g,
107
- };
108
- contentModules = {
109
- replace: (tag, module, content) => {
110
- let open = module.open;
111
- let close = module.close;
112
- if(typeof(open) === 'function') {
113
- open = open(tag.attr);
114
- }
115
- if(typeof(close) === 'function') {
116
- close = close(tag.attr);
117
- }
118
- // do the replace
119
- if(open && !tag.isClosing) {
120
- //content = content
121
- content = content.replace('[TAG-' + tag.index + ']', open);
122
- }
123
- if(close && tag.closing) {
124
- content = content.replace('[TAG-' + tag.closing.index + ']', close);
125
- }
126
- return content;
127
- },
128
- content: (tag, module, content) => {
129
- if(!tag.closing) { return content; }
130
- const openTag = '[TAG-' + tag.index + ']';
131
- const closeTag = '[TAG-' + tag.closing.index + ']';
132
- const start = content.indexOf(openTag);
133
- const end = content.indexOf(closeTag);
134
- let replace = module.replace;
135
-
136
- const innerContent = content.slice(start + openTag.length, end);
137
- if(typeof(replace) === 'function') {
138
- replace = replace(tag.attr, innerContent);
139
- }
140
-
141
- const contentStart = content.slice(0, Math.max(0, start));
142
- const contentEnd = content.slice(end + closeTag.length);
143
-
144
- return contentStart + replace + contentEnd;
145
- },
146
- ignore: (tag, module, content) => {
147
- const openTag = '[TAG-' + tag.index + ']';
148
- const start = content.indexOf(openTag);
149
- let closeTag = '';
150
- let end = content.length;
151
- if(tag.closing) {
152
- closeTag = '[TAG-' + tag.closing.index + ']';
153
- end = content.indexOf(closeTag);
154
- }
155
- let innerContent = content.slice(start + openTag.length, end);
156
- innerContent = this.#ignoreLoop(tag.children, innerContent);
157
- const contentStart = content.slice(0, Math.max(0, start));
158
- const contentEnd = content.slice(end + closeTag.length);
159
- return contentStart + innerContent + contentEnd;
160
- },
161
- };
162
- constructor(config = {}) {
163
- this.config = {
164
- newline: true,
165
- paragraph: false,
166
- cleanUnmatchable: true,
167
- sanitizeHtml: true,
168
- };
169
- if(config.newline !== undefined) {
170
- this.config.newline = config.newline;
171
- }
172
- if(config.paragraph !== undefined) {
173
- this.config.paragraph = config.paragraph;
174
- }
175
- if(config.cleanUnmatchable !== undefined) {
176
- this.config.cleanUnmatchable = config.cleanUnmatchable;
177
- }
178
- if(config.sanitizeHtml !== undefined) {
179
- this.config.sanitizeHtml = config.sanitizeHtml;
180
- }
181
- }
182
-
183
- #ignoreLoop = function(tagsMap, content) {
184
- for(const tag of tagsMap) {
185
- content = content.replace('[TAG-' + tag.index + ']', tag.raw);
186
- if(tag.closing) {
187
- content = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);
188
- }
189
- if(tag.children.length > 0) {
190
- content = this.#ignoreLoop(tag.children, content);
191
- }
192
- }
193
- return content;
194
- };
195
-
196
- #contentLoop = function(tagsMap, content) {
197
- for(const tag of tagsMap) {
198
- let module = this.tags[tag.module];
199
- if(!module) {
200
- // ignore invalid BBCode
201
- module = {
202
- type: 'replace',
203
- open: tag.raw,
204
- close: tag.closing && tag.closing.raw || '',
205
- };
206
- }
207
- if(!this.contentModules[module.type]) {
208
- throw new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');
209
- }
210
- content = this.contentModules[module.type](tag, module, content);
211
- if(tag.children.length > 0 && module.type !== 'ignore') {
212
- content = this.#contentLoop(tag.children, content);
213
- }
214
- }
215
-
216
- return content;
217
- };
218
-
219
- #tagLoop = function(tagsMap, parent) {
220
- let currentTagIndex = 0;
221
- while(currentTagIndex < tagsMap.length) {
222
- let found = false;
223
- if(tagsMap[currentTagIndex].matchTag !== null || tagsMap[currentTagIndex].isClosing) {
224
- currentTagIndex++;
225
- continue; // already handled this tag / not closing
226
- }
227
- for(const [i, item] of tagsMap.entries()) {
228
- if(
229
- found ||
230
- tagsMap[currentTagIndex].matchTag !== null ||
231
- item.index === tagsMap[currentTagIndex].index ||
232
- item.matchTag !== null ||
233
- !item.isClosing ||
234
- tagsMap[currentTagIndex].module !== item.module
235
- ) {
236
- continue;
237
- }
238
- tagsMap[i].matchTag = tagsMap[currentTagIndex].index;
239
- tagsMap[currentTagIndex].matchTag = item.index;
240
- found = i; // next index
241
- }
242
-
243
- const childStart = currentTagIndex + 1;
244
-
245
- if(found !== false) {
246
- tagsMap[currentTagIndex].closing = tagsMap[tagsMap[currentTagIndex].matchTag];
247
- }else{
248
- tagsMap[currentTagIndex].matchTag = false;
249
- found = tagsMap.length - 1;
250
- }
251
-
252
- // sweep children
253
- if(childStart < found) {
254
- tagsMap[currentTagIndex].children = tagsMap.slice(childStart, found).map((child) => {
255
- child.parent = tagsMap[currentTagIndex].index;
256
- return child;
257
- });
258
- }
259
-
260
- let i = childStart;
261
- while(i < found) {
262
- tagsMap[i].parent = tagsMap[currentTagIndex].index;
263
- i++;
264
- }
265
-
266
- currentTagIndex++; // move on
267
- }
268
-
269
- // sweep children & matched closing tags
270
- // TODO: make this more readable
271
- tagsMap = tagsMap.filter(item => !((parent === undefined && item.parent !== null) || (item.parent !== null && item.parent !== parent) || item.isClosing));
272
-
273
- return tagsMap.map((tag) => {
274
- if(tag.children.length > 0) {
275
- tag.children = this.#tagLoop(tag.children, tag.index);
276
- }
277
- return tag;
278
- });
279
- };
280
-
281
- clearTags() {
282
- this.tags = {};
283
- return this;
284
- }
285
-
286
- registerTag(tag, options) {
287
- this.tags[String(tag).toLowerCase()] = options;
288
- return this;
289
- }
290
-
291
- parse(bbcInput) {
292
- if(typeof(bbcInput) === 'boolean' || typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput))) { return ''; }
293
- // eslint-disable-next-line unicorn/prefer-spread
294
- let input = String(bbcInput).slice(0); // cheap string clone
295
- if(this.config.sanitizeHtml) {
296
- input = input
297
- .replaceAll('&', '&amp;')
298
- .replaceAll('<', '&lt;')
299
- .replaceAll('>', '&gt;')
300
- .replaceAll('"', '&quot;')
301
- .replaceAll('\'', '&#39;');
302
- }
303
-
304
- // reset
305
- let tagsMap = [];
306
- // split input into tags by index
307
- const tags = String(input).match(this.regex.tags);
308
-
309
- if(this.config.newline) {
310
- if(this.config.paragraph) {
311
- input = input.replace(this.regex.newline, '</p><p>');
312
- }else{
313
- input = input.replace(this.regex.newline, '<br/>');
314
- }
315
- }
316
- if(this.config.paragraph) {
317
- input = '<p>' + input + '</p>';
318
- }
319
-
320
- // handle when no tags are present
321
- if(!tags || tags.length === 0) {
322
- return input;
323
- }
324
- for(const [i, tag] of tags.entries()) {
325
- const parts = tag.slice(1, -1).split('=');
326
- const item = {
327
- index: i,
328
- module: parts[0].toLowerCase(),
329
- isClosing: tag.slice(1, 2) === '/',
330
- raw: tag,
331
- attr: parts.slice(1).join('='),
332
- closing: null,
333
- children: [],
334
- parent: null,
335
- matchTag: null,
336
- };
337
- if(item.isClosing) {
338
- item.module = item.module.slice(1);
339
- }
340
-
341
- tagsMap.push(item);
342
- input = input.replace(tag, '[TAG-' + i + ']'); // placeholder for tag
343
- }
344
- // loop through each tag to create nested elements
345
- tagsMap = this.#tagLoop(tagsMap);
346
- // put back all non-found matches?
347
- input = this.#contentLoop(tagsMap, input);
348
- if(this.config.cleanUnmatchable) {
349
- input = input.replace(this.regex.placeholders, '');
350
- }
351
- return input;
352
- }
353
- }
354
-
355
- module.exports = yabbcode;