ya-bbcode 1.0.11 → 3.0.1

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.
Files changed (4) hide show
  1. package/README.md +44 -45
  2. package/package.json +56 -43
  3. package/ya-bbcode.d.ts +38 -0
  4. package/ya-bbcode.js +342 -350
package/README.md CHANGED
@@ -1,45 +1,44 @@
1
- # ya-bbcode
2
- Yet another BBCode parser.
3
- `npm install ya-bbcode --save`
4
-
5
- [![npm version](https://badge.fury.io/js/ya-bbcode.svg)](https://badge.fury.io/js/ya-bbcode)
6
- [![dependencies Status](https://david-dm.org/nodecraft/ya-bbcode/status.svg)](https://david-dm.org/nodecraft/ya-bbcode)
7
- [![Actions Status](https://github.com/nodecraft/ya-bbcode/workflows/Test/badge.svg)](https://github.com/nodecraft/ya-bbcode/actions)
8
- [![Coverage Status](https://coveralls.io/repos/github/nodecraft/ya-bbcode/badge.svg)](https://coveralls.io/github/nodecraft/ya-bbcode)
9
-
10
- ## Usage
11
-
12
- ```javascript
13
- const yabbcode = require('ya-bbcode');
14
- let parser = new yabbcode();
15
-
16
- let bbc = '[url=https://nodecraft.com]Visit Nodecraft[/url]';
17
- parser.parse(bbc);
18
- // <a href="https://nodecraft.com">Visit Nodecraft</a>
19
- ```
20
-
21
- ##### Add Custom Tags
22
-
23
- ```javascript
24
- parser.registerTag('url', {
25
- type: 'replace',
26
- open: (attr) => {
27
- return `<a href="${attr || '#'}" rel="noopener norefer">`;
28
- },
29
- close: '</a>'
30
- });
31
-
32
- // Remove all default or registered tags
33
- parser.clearTags();
34
- ```
35
-
36
- ### Why another BBCode Parser?
37
- - Supports nested BBCode
38
- - Has no dependencies
39
- - All BBCode is replaced in a nested format, meaning that parent nodes are parsed before children.
40
- - Allows custom tags to be replaced or added.
41
-
42
- #### Roadmap
43
- - Performance improvements
44
- - Clean code up for improved readability
45
- - Improve docs
1
+ # ya-bbcode
2
+ Yet another BBCode parser.
3
+ `npm install ya-bbcode --save`
4
+
5
+ [![npm version](https://badge.fury.io/js/ya-bbcode.svg)](https://badge.fury.io/js/ya-bbcode)
6
+ [![Actions Status](https://github.com/nodecraft/ya-bbcode/workflows/Test/badge.svg)](https://github.com/nodecraft/ya-bbcode/actions)
7
+ [![Coverage Status](https://coveralls.io/repos/github/nodecraft/ya-bbcode/badge.svg)](https://coveralls.io/github/nodecraft/ya-bbcode)
8
+
9
+ ## Usage
10
+
11
+ ```javascript
12
+ const yabbcode = require('ya-bbcode');
13
+ const parser = new yabbcode();
14
+
15
+ const bbc = '[url=https://nodecraft.com]Visit Nodecraft[/url]';
16
+ parser.parse(bbc);
17
+ // <a href="https://nodecraft.com">Visit Nodecraft</a>
18
+ ```
19
+
20
+ ##### Add Custom Tags
21
+
22
+ ```javascript
23
+ parser.registerTag('url', {
24
+ type: 'replace',
25
+ open: (attr) => {
26
+ return `<a href="${attr || '#'}" rel="noopener norefer">`;
27
+ },
28
+ close: '</a>'
29
+ });
30
+
31
+ // Remove all default or registered tags
32
+ parser.clearTags();
33
+ ```
34
+
35
+ ### Why another BBCode Parser?
36
+ - Supports nested BBCode
37
+ - Has no dependencies
38
+ - All BBCode is replaced in a nested format, meaning that parent nodes are parsed before children.
39
+ - Allows custom tags to be replaced or added.
40
+
41
+ #### Roadmap
42
+ - Performance improvements
43
+ - Clean code up for improved readability
44
+ - Improve docs
package/package.json CHANGED
@@ -1,43 +1,56 @@
1
- {
2
- "name": "ya-bbcode",
3
- "version": "1.0.11",
4
- "description": "Yet another BBCode Parser",
5
- "keywords": [
6
- "bbc",
7
- "bbcode",
8
- "html",
9
- "parser"
10
- ],
11
- "homepage": "https://github.com/nodecraft/ya-bbcode",
12
- "bugs": {
13
- "url": "https://github.com/nodecraft/ya-bbcode/issues"
14
- },
15
- "repository": {
16
- "type": "git",
17
- "url": "https://github.com/nodecraft/ya-bbcode"
18
- },
19
- "license": "MIT",
20
- "author": "Nodecraft Inc.",
21
- "files": [
22
- "ya-bbcode.js"
23
- ],
24
- "main": "ya-bbcode.js",
25
- "scripts": {
26
- "coverage": "nyc report --reporter=lcov",
27
- "lint": "eslint .",
28
- "test": "npm run lint && nyc ava"
29
- },
30
- "dependencies": {},
31
- "devDependencies": {
32
- "ava": "^2.4.0",
33
- "coveralls": "^3.0.7",
34
- "eslint": "^6.5.1",
35
- "eslint-config-nodecraft": "^1.7.3",
36
- "eslint-plugin-json": "^1.4.0",
37
- "eslint-plugin-node": "^10.0.0",
38
- "nyc": "^14.1.1"
39
- },
40
- "engines": {
41
- "node": ">=8"
42
- }
43
- }
1
+ {
2
+ "name": "ya-bbcode",
3
+ "version": "3.0.1",
4
+ "description": "Yet another BBCode Parser",
5
+ "keywords": [
6
+ "bbc",
7
+ "bbcode",
8
+ "html",
9
+ "parser"
10
+ ],
11
+ "homepage": "https://github.com/nodecraft/ya-bbcode",
12
+ "bugs": {
13
+ "url": "https://github.com/nodecraft/ya-bbcode/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/nodecraft/ya-bbcode"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Nodecraft Inc.",
21
+ "main": "ya-bbcode.js",
22
+ "types": "ya-bbcode.d.ts",
23
+ "files": [
24
+ "ya-bbcode.js",
25
+ "ya-bbcode.d.ts"
26
+ ],
27
+ "scripts": {
28
+ "coverage": "nyc report --reporter=lcov",
29
+ "lint": "eslint . --fix",
30
+ "nyc": "nyc ava",
31
+ "prepare": "husky install",
32
+ "test": "npm run lint && nyc ava"
33
+ },
34
+ "lint-staged": {
35
+ "*.js": "eslint --cache"
36
+ },
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "@commitlint/cli": "^17.0.3",
40
+ "@commitlint/config-conventional": "^17.0.3",
41
+ "@nodecraft/eslint-config": "^19.0.0",
42
+ "ava": "^4.3.1",
43
+ "coveralls": "^3.1.1",
44
+ "eslint": "^8.22.0",
45
+ "eslint-plugin-json": "^3.1.0",
46
+ "eslint-plugin-node": "^11.1.0",
47
+ "eslint-plugin-unicorn": "^43.0.2",
48
+ "husky": "^8.0.1",
49
+ "lint-staged": "^13.0.3",
50
+ "nyc": "^15.1.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=14",
54
+ "npm": ">=8"
55
+ }
56
+ }
package/ya-bbcode.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export = yabbcode
2
+
3
+ declare class yabbcode {
4
+ /**
5
+ * Create a new bbcode parser
6
+ */
7
+ constructor(config?: {
8
+ newline?: boolean
9
+ paragraph?: boolean
10
+ cleanUnmatchable?: boolean
11
+ })
12
+ /**
13
+ * Parse bbcode
14
+ * @param bbc bbcode to parse
15
+ */
16
+ parse(bbc: any): string
17
+ /**
18
+ * Remove all default or registered tags
19
+ */
20
+ clearTags(): this
21
+ /**
22
+ * Add custom tags
23
+ * @param tag tag name
24
+ * @param options options to parse the tag
25
+ */
26
+ registerTag(tag: string, options: {
27
+ type: 'replace'
28
+ open: ((attr: string) => string) | string
29
+ close: ((attr: string) => string) | string | null
30
+ }): this
31
+ registerTag(tag: string, options: {
32
+ type: 'content',
33
+ replace: (attr: string, content: string) => string
34
+ }): this
35
+ registerTag(tag: string, options: {
36
+ type: 'ignore'
37
+ }): this
38
+ }
package/ya-bbcode.js CHANGED
@@ -1,350 +1,342 @@
1
- 'use strict';
2
-
3
- const yabbcode = function(config = {}){
4
- if(!(this instanceof yabbcode)){
5
- return new yabbcode();
6
- }
7
- let self = this;
8
- this.config = {
9
- newline: true,
10
- paragraph: false,
11
- cleanUnmatchable: true
12
- };
13
- if(config.newline !== undefined){
14
- this.config.newline = config.newline;
15
- }
16
- if(config.paragraph !== undefined){
17
- this.config.paragraph = config.paragraph;
18
- }
19
- if(config.cleanUnmatchable !== undefined){
20
- this.config.cleanUnmatchable = config.cleanUnmatchable;
21
- }
22
-
23
- this.tags = {
24
- 'url': {
25
- type: 'replace',
26
- open: (attr) => {
27
- return `<a href="${attr || '#'}">`;
28
- },
29
- close: '</a>'
30
- },
31
- 'quote': {
32
- type: 'replace',
33
- open: (attr) => {
34
- return `<blockquote author="${attr || ''}">`;
35
- },
36
- close: '</blockquote>'
37
- },
38
- 'b': {
39
- type: 'replace',
40
- open: '<strong>',
41
- close: '</strong>'
42
- },
43
- 'u': {
44
- type: 'replace',
45
- open: '<u>',
46
- close: '</u>'
47
- },
48
- 'i': {
49
- type: 'replace',
50
- open: '<i>',
51
- close: '</i>'
52
- },
53
- 'h1': {
54
- type: 'replace',
55
- open: '<h1>',
56
- close: '</h1>'
57
- },
58
- 'h2': {
59
- type: 'replace',
60
- open: '<h2>',
61
- close: '</h2>'
62
- },
63
- 'h3': {
64
- type: 'replace',
65
- open: '<h3>',
66
- close: '</h3>'
67
- },
68
- 'h4': {
69
- type: 'replace',
70
- open: '<h4>',
71
- close: '</h4>'
72
- },
73
- 'h5': {
74
- type: 'replace',
75
- open: '<h5>',
76
- close: '</h5>'
77
- },
78
- 'h6': {
79
- type: 'replace',
80
- open: '<h6>',
81
- close: '</h6>'
82
- },
83
- 'code': {
84
- type: 'replace',
85
- open: '<code>',
86
- close: '</code>'
87
- },
88
- 'strike': {
89
- type: 'replace',
90
- open: '<span class="yabbcode-strike">',
91
- close: '</span>'
92
- },
93
- 'spoiler': {
94
- type: 'replace',
95
- open: '<span class="yabbcode-spoiler">',
96
- close: '</span>'
97
- },
98
- 'list': {
99
- type: 'replace',
100
- open: '<ul>',
101
- close: '</ul>'
102
- },
103
- 'olist': {
104
- type: 'replace',
105
- open: '<ol>',
106
- close: '</ol>'
107
- },
108
- '*': {
109
- type: 'replace',
110
- open: '<li>',
111
- close: null
112
- },
113
- 'img': {
114
- type: 'content',
115
- replace: (attr, content) => {
116
- if(!content){
117
- return '';
118
- }
119
- return `<img src="${content}" alt="${attr || ''}"/>`;
120
- }
121
- },
122
- 'noparse': {
123
- type: 'ignore'
124
- }
125
- };
126
- this.contentModules = {
127
- replace: (tag, module, content) => {
128
- let open = module.open,
129
- close = module.close;
130
- if(typeof(open) === 'function'){
131
- open = open(tag.attr);
132
- }
133
- if(typeof(close) === 'function'){
134
- close = close(tag.attr);
135
- }
136
- // do the replace
137
- if(open && !tag.isClosing){
138
- //content = content
139
- content = content.replace('[TAG-' + tag.index + ']', open);
140
- }
141
- if(close && tag.closing){
142
- content = content.replace('[TAG-' + tag.closing.index + ']', close);
143
- }
144
- return content;
145
- },
146
- content: (tag, module, content) => {
147
- if(!tag.closing){ return content; }
148
- let openTag = "[TAG-" + tag.index + "]",
149
- closeTag = "[TAG-" + tag.closing.index + "]";
150
- let start = content.indexOf(openTag),
151
- end = content.indexOf(closeTag),
152
- replace = module.replace;
153
-
154
- let innerContent = content.substr(start + openTag.length, end - (start + openTag.length));
155
- if(typeof(replace) === 'function'){
156
- replace = replace(tag.attr, innerContent);
157
- }
158
-
159
- let contentStart = content.substr(0, start),
160
- contentEnd = content.substr(end + closeTag.length);
161
-
162
- return contentStart + replace + contentEnd;
163
- },
164
- ignore: (tag, module, content) => {
165
- let openTag = "[TAG-" + tag.index + "]";
166
- let start = content.indexOf(openTag);
167
- let closeTag = "";
168
- let end = content.length;
169
- if(tag.closing){
170
- closeTag = "[TAG-" + tag.closing.index + "]";
171
- end = content.indexOf(closeTag);
172
- }
173
- let innerContent = content.substr(start + openTag.length, end - (start + openTag.length));
174
- innerContent = self._ignoreLoop(tag.children, innerContent);
175
- let contentStart = content.substr(0, start),
176
- contentEnd = content.substr(end + closeTag.length);
177
- return contentStart + innerContent + contentEnd;
178
- }
179
- };
180
-
181
- this.regex = {
182
- tags: /(\[[^\]^\s]{1,}\])/g,
183
- newline: /(?:\r\n|\r|\n)/g,
184
- placeholders: /\[TAG-[1-9]{1,}\]/g
185
- };
186
- };
187
- yabbcode.prototype._ignoreLoop = function(tagsMap, content){
188
- tagsMap.forEach((tag) => {
189
- content = content.replace('[TAG-' + tag.index + ']', tag.raw);
190
- if(tag.closing){
191
- content = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);
192
- }
193
- if(tag.children.length){
194
- content = this._ignoreLoop(tag.children, content);
195
- }
196
- });
197
- return content;
198
- };
199
-
200
- yabbcode.prototype._contentLoop = function(tagsMap, content){
201
- tagsMap.forEach((tag) => {
202
- let module = this.tags[tag.module];
203
- if(!module){
204
- // ignore invalid BBCode
205
- module = {
206
- type: 'replace',
207
- open: tag.raw,
208
- close: tag.closing && tag.closing.raw || ''
209
- };
210
- }
211
- if(!this.contentModules[module.type]){
212
- throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
213
- }
214
- content = this.contentModules[module.type](tag, module, content);
215
- if(tag.children.length && module.type !== 'ignore'){
216
- content = this._contentLoop(tag.children, content);
217
- }
218
- });
219
-
220
- return content;
221
- };
222
-
223
- yabbcode.prototype._tagLoop = function(tagsMap, parent){
224
- let currentTagIndex = 0;
225
- while(currentTagIndex < tagsMap.length){
226
- let found = false;
227
- if(tagsMap[currentTagIndex].matchTag !== null || tagsMap[currentTagIndex].isClosing){
228
- currentTagIndex++;
229
- continue; // already handled this tag / not closing
230
- }
231
- tagsMap.forEach((item, i) => {
232
- if(
233
- found ||
234
- tagsMap[currentTagIndex].matchTag !== null ||
235
- item.index === tagsMap[currentTagIndex].index ||
236
- item.matchTag !== null ||
237
- !item.isClosing ||
238
- tagsMap[currentTagIndex].module !== item.module
239
- ){
240
- return;
241
- }
242
- tagsMap[i].matchTag = tagsMap[currentTagIndex].index;
243
- tagsMap[currentTagIndex].matchTag = item.index;
244
- found = i; // next index
245
- });
246
-
247
- let childStart = currentTagIndex + 1;
248
-
249
- if(found !== false){
250
- tagsMap[currentTagIndex].closing = tagsMap[tagsMap[currentTagIndex].matchTag];
251
- }else{
252
- tagsMap[currentTagIndex].matchTag = false;
253
- found = tagsMap.length - 1;
254
- }
255
-
256
- // sweep children
257
- if(childStart < found){
258
- tagsMap[currentTagIndex].children = tagsMap.slice(childStart, found).map((child) => {
259
- child.parent = tagsMap[currentTagIndex].index;
260
- return child;
261
- });
262
- }
263
-
264
- let i = childStart;
265
- while(i < found){
266
- tagsMap[i].parent = tagsMap[currentTagIndex].index;
267
- i++;
268
- }
269
-
270
- currentTagIndex++; // move on
271
- }
272
-
273
- // sweep children & matched closing tags
274
- tagsMap = tagsMap.filter((item) => {
275
- // TODO: make this more readable
276
- return !((parent === undefined && item.parent !== null) || (item.parent !== null && item.parent !== parent) || item.isClosing);
277
- });
278
-
279
- return tagsMap.map((tag) => {
280
- if(tag.children.length){
281
- tag.children = this._tagLoop(tag.children, tag.index);
282
- }
283
- return tag;
284
- });
285
- };
286
-
287
- yabbcode.prototype.clearTags = function(){
288
- this.tags = {};
289
- return this;
290
- };
291
-
292
- yabbcode.prototype.registerTag = function(tag, options){
293
- this.tags[String(tag).toLowerCase()] = options;
294
- return this;
295
- };
296
-
297
- yabbcode.prototype.parse = function(bbcInput){
298
- if(typeof(bbcInput) === 'boolean' || typeof(bbcInput) !== 'string' && isNaN(bbcInput)){ return ''; }
299
- let input = String(bbcInput).slice(0); // cheap string clone
300
- // reset
301
- let tagsMap = [];
302
- // split input into tags by index
303
- let tags = String(input).match(this.regex.tags);
304
-
305
- if(this.config.newline){
306
- if(this.config.paragraph){
307
- input = input.replace(this.regex.newline, "</p><p>");
308
- }else{
309
- input = input.replace(this.regex.newline, "<br/>");
310
- }
311
- }
312
- if(this.config.paragraph){
313
- input = '<p>' + input + '</p>';
314
- }
315
-
316
- // handle when no tags are present
317
- if(!tags || !tags.length){
318
- return input;
319
- }
320
- tags.forEach((tag, i) => {
321
- let parts = tag.slice(1, -1).split('=');
322
- let item = {
323
- index: i,
324
- module: parts[0].toLowerCase(),
325
- isClosing: tag.slice(1, 2) === '/',
326
- raw: tag,
327
- attr: parts[1],
328
- closing: null,
329
- children: [],
330
- parent: null,
331
- matchTag: null
332
- };
333
- if(item.isClosing){
334
- item.module = item.module.slice(1);
335
- }
336
-
337
- tagsMap.push(item);
338
- input = input.replace(tag, '[TAG-' + i + ']'); // placeholder for tag
339
- });
340
- // loop through each tag to create nested elements
341
- tagsMap = this._tagLoop(tagsMap);
342
- // put back all non-found matches?
343
- input = this._contentLoop(tagsMap, input);
344
- if(this.config.cleanUnmatchable){
345
- input = input.replace(this.regex.placeholders, '');
346
- }
347
- return input;
348
- };
349
-
350
- module.exports = yabbcode;
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: /(\[[^\s\]^]+])/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
+ };
168
+ if(config.newline !== undefined){
169
+ this.config.newline = config.newline;
170
+ }
171
+ if(config.paragraph !== undefined){
172
+ this.config.paragraph = config.paragraph;
173
+ }
174
+ if(config.cleanUnmatchable !== undefined){
175
+ this.config.cleanUnmatchable = config.cleanUnmatchable;
176
+ }
177
+ }
178
+
179
+ #ignoreLoop = function(tagsMap, content){
180
+ for(const tag of tagsMap){
181
+ content = content.replace('[TAG-' + tag.index + ']', tag.raw);
182
+ if(tag.closing){
183
+ content = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);
184
+ }
185
+ if(tag.children.length > 0){
186
+ content = this.#ignoreLoop(tag.children, content);
187
+ }
188
+ }
189
+ return content;
190
+ };
191
+
192
+ #contentLoop = function(tagsMap, content){
193
+ for(const tag of tagsMap){
194
+ let module = this.tags[tag.module];
195
+ if(!module){
196
+ // ignore invalid BBCode
197
+ module = {
198
+ type: 'replace',
199
+ open: tag.raw,
200
+ close: tag.closing && tag.closing.raw || '',
201
+ };
202
+ }
203
+ if(!this.contentModules[module.type]){
204
+ throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
205
+ }
206
+ content = this.contentModules[module.type](tag, module, content);
207
+ if(tag.children.length > 0 && module.type !== 'ignore'){
208
+ content = this.#contentLoop(tag.children, content);
209
+ }
210
+ }
211
+
212
+ return content;
213
+ };
214
+
215
+ #tagLoop = function(tagsMap, parent){
216
+ let currentTagIndex = 0;
217
+ while(currentTagIndex < tagsMap.length){
218
+ let found = false;
219
+ if(tagsMap[currentTagIndex].matchTag !== null || tagsMap[currentTagIndex].isClosing){
220
+ currentTagIndex++;
221
+ continue; // already handled this tag / not closing
222
+ }
223
+ for(const [i, item] of tagsMap.entries()){
224
+ if(
225
+ found ||
226
+ tagsMap[currentTagIndex].matchTag !== null ||
227
+ item.index === tagsMap[currentTagIndex].index ||
228
+ item.matchTag !== null ||
229
+ !item.isClosing ||
230
+ tagsMap[currentTagIndex].module !== item.module
231
+ ){
232
+ continue;
233
+ }
234
+ tagsMap[i].matchTag = tagsMap[currentTagIndex].index;
235
+ tagsMap[currentTagIndex].matchTag = item.index;
236
+ found = i; // next index
237
+ }
238
+
239
+ const childStart = currentTagIndex + 1;
240
+
241
+ if(found !== false){
242
+ tagsMap[currentTagIndex].closing = tagsMap[tagsMap[currentTagIndex].matchTag];
243
+ }else{
244
+ tagsMap[currentTagIndex].matchTag = false;
245
+ found = tagsMap.length - 1;
246
+ }
247
+
248
+ // sweep children
249
+ if(childStart < found){
250
+ tagsMap[currentTagIndex].children = tagsMap.slice(childStart, found).map((child) => {
251
+ child.parent = tagsMap[currentTagIndex].index;
252
+ return child;
253
+ });
254
+ }
255
+
256
+ let i = childStart;
257
+ while(i < found){
258
+ tagsMap[i].parent = tagsMap[currentTagIndex].index;
259
+ i++;
260
+ }
261
+
262
+ currentTagIndex++; // move on
263
+ }
264
+
265
+ // sweep children & matched closing tags
266
+ // TODO: make this more readable
267
+ tagsMap = tagsMap.filter(item => !((parent === undefined && item.parent !== null) || (item.parent !== null && item.parent !== parent) || item.isClosing));
268
+
269
+ return tagsMap.map((tag) => {
270
+ if(tag.children.length > 0){
271
+ tag.children = this.#tagLoop(tag.children, tag.index);
272
+ }
273
+ return tag;
274
+ });
275
+ };
276
+
277
+ clearTags(){
278
+ this.tags = {};
279
+ return this;
280
+ }
281
+
282
+ registerTag(tag, options){
283
+ this.tags[String(tag).toLowerCase()] = options;
284
+ return this;
285
+ }
286
+
287
+ parse(bbcInput){
288
+ if(typeof(bbcInput) === 'boolean' || typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput))){ return ''; }
289
+ // eslint-disable-next-line unicorn/prefer-spread
290
+ let input = String(bbcInput).slice(0); // cheap string clone
291
+ // reset
292
+ let tagsMap = [];
293
+ // split input into tags by index
294
+ const tags = String(input).match(this.regex.tags);
295
+
296
+ if(this.config.newline){
297
+ if(this.config.paragraph){
298
+ input = input.replace(this.regex.newline, "</p><p>");
299
+ }else{
300
+ input = input.replace(this.regex.newline, "<br/>");
301
+ }
302
+ }
303
+ if(this.config.paragraph){
304
+ input = '<p>' + input + '</p>';
305
+ }
306
+
307
+ // handle when no tags are present
308
+ if(!tags || tags.length === 0){
309
+ return input;
310
+ }
311
+ for(const [i, tag] of tags.entries()){
312
+ const parts = tag.slice(1, -1).split('=');
313
+ const item = {
314
+ index: i,
315
+ module: parts[0].toLowerCase(),
316
+ isClosing: tag.slice(1, 2) === '/',
317
+ raw: tag,
318
+ attr: parts.slice(1).join('='),
319
+ closing: null,
320
+ children: [],
321
+ parent: null,
322
+ matchTag: null,
323
+ };
324
+ if(item.isClosing){
325
+ item.module = item.module.slice(1);
326
+ }
327
+
328
+ tagsMap.push(item);
329
+ input = input.replace(tag, '[TAG-' + i + ']'); // placeholder for tag
330
+ }
331
+ // loop through each tag to create nested elements
332
+ tagsMap = this.#tagLoop(tagsMap);
333
+ // put back all non-found matches?
334
+ input = this.#contentLoop(tagsMap, input);
335
+ if(this.config.cleanUnmatchable){
336
+ input = input.replace(this.regex.placeholders, '');
337
+ }
338
+ return input;
339
+ }
340
+ }
341
+
342
+ module.exports = yabbcode;