bbcode-compiler 0.1.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/generateHtml.d.ts +2 -0
  4. package/dist/generateHtml.d.ts.map +1 -0
  5. package/dist/generateHtml.js +13 -0
  6. package/dist/generateHtml.js.map +1 -0
  7. package/dist/generator/Generator.d.ts +8 -0
  8. package/dist/generator/Generator.d.ts.map +1 -0
  9. package/dist/generator/Generator.js +54 -0
  10. package/dist/generator/Generator.js.map +1 -0
  11. package/dist/generator/transforms/Transform.d.ts +10 -0
  12. package/dist/generator/transforms/Transform.d.ts.map +1 -0
  13. package/dist/generator/transforms/Transform.js +2 -0
  14. package/dist/generator/transforms/Transform.js.map +1 -0
  15. package/dist/generator/transforms/htmlTransforms.d.ts +3 -0
  16. package/dist/generator/transforms/htmlTransforms.d.ts.map +1 -0
  17. package/dist/generator/transforms/htmlTransforms.js +198 -0
  18. package/dist/generator/transforms/htmlTransforms.js.map +1 -0
  19. package/dist/generator/utils/getTagImmediateAttrVal.d.ts +14 -0
  20. package/dist/generator/utils/getTagImmediateAttrVal.d.ts.map +1 -0
  21. package/dist/generator/utils/getTagImmediateAttrVal.js +19 -0
  22. package/dist/generator/utils/getTagImmediateAttrVal.js.map +1 -0
  23. package/dist/generator/utils/getTagImmediateText.d.ts +12 -0
  24. package/dist/generator/utils/getTagImmediateText.d.ts.map +1 -0
  25. package/dist/generator/utils/getTagImmediateText.js +29 -0
  26. package/dist/generator/utils/getTagImmediateText.js.map +1 -0
  27. package/dist/generator/utils/getWidthHeightAttr.d.ts +31 -0
  28. package/dist/generator/utils/getWidthHeightAttr.d.ts.map +1 -0
  29. package/dist/generator/utils/getWidthHeightAttr.js +47 -0
  30. package/dist/generator/utils/getWidthHeightAttr.js.map +1 -0
  31. package/dist/generator/utils/isDangerousUrl.d.ts +2 -0
  32. package/dist/generator/utils/isDangerousUrl.d.ts.map +1 -0
  33. package/dist/generator/utils/isDangerousUrl.js +14 -0
  34. package/dist/generator/utils/isDangerousUrl.js.map +1 -0
  35. package/dist/generator/utils/isOrderedList.d.ts +19 -0
  36. package/dist/generator/utils/isOrderedList.d.ts.map +1 -0
  37. package/dist/generator/utils/isOrderedList.js +26 -0
  38. package/dist/generator/utils/isOrderedList.js.map +1 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +16 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/lexer/Lexer.d.ts +5 -0
  44. package/dist/lexer/Lexer.d.ts.map +1 -0
  45. package/dist/lexer/Lexer.js +81 -0
  46. package/dist/lexer/Lexer.js.map +1 -0
  47. package/dist/lexer/Token.d.ts +8 -0
  48. package/dist/lexer/Token.d.ts.map +1 -0
  49. package/dist/lexer/Token.js +54 -0
  50. package/dist/lexer/Token.js.map +1 -0
  51. package/dist/lexer/TokenType.d.ts +17 -0
  52. package/dist/lexer/TokenType.d.ts.map +1 -0
  53. package/dist/lexer/TokenType.js +41 -0
  54. package/dist/lexer/TokenType.js.map +1 -0
  55. package/dist/parser/AstNode.d.ts +105 -0
  56. package/dist/parser/AstNode.d.ts.map +1 -0
  57. package/dist/parser/AstNode.js +263 -0
  58. package/dist/parser/AstNode.js.map +1 -0
  59. package/dist/parser/Parser.d.ts +11 -0
  60. package/dist/parser/Parser.d.ts.map +1 -0
  61. package/dist/parser/Parser.js +265 -0
  62. package/dist/parser/Parser.js.map +1 -0
  63. package/dist/parser/nodeIsType.d.ts +13 -0
  64. package/dist/parser/nodeIsType.d.ts.map +1 -0
  65. package/dist/parser/nodeIsType.js +5 -0
  66. package/dist/parser/nodeIsType.js.map +1 -0
  67. package/package.json +68 -0
  68. package/src/generateHtml.ts +15 -0
  69. package/src/generator/Generator.ts +60 -0
  70. package/src/generator/transforms/Transform.ts +15 -0
  71. package/src/generator/transforms/htmlTransforms.ts +205 -0
  72. package/src/generator/utils/getTagImmediateAttrVal.ts +21 -0
  73. package/src/generator/utils/getTagImmediateText.ts +33 -0
  74. package/src/generator/utils/getWidthHeightAttr.ts +51 -0
  75. package/src/generator/utils/isDangerousUrl.ts +17 -0
  76. package/src/generator/utils/isOrderedList.ts +28 -0
  77. package/src/index.ts +18 -0
  78. package/src/lexer/Lexer.ts +89 -0
  79. package/src/lexer/Token.ts +64 -0
  80. package/src/lexer/TokenType.ts +65 -0
  81. package/src/parser/AstNode.ts +338 -0
  82. package/src/parser/Parser.ts +316 -0
  83. package/src/parser/nodeIsType.ts +15 -0
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "bbcode-compiler",
3
+ "version": "0.1.0",
4
+ "description": "Fast BBCode parser and HTML generator with TypeScript support",
5
+ "exports": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "README.md",
9
+ "dist/*",
10
+ "src/*"
11
+ ],
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "repository": "https://github.com/Trinovantes/bbcode-compiler",
15
+ "author": "Stephen Li <hello@stephenli.ca> (https://www.stephenli.ca)",
16
+ "license": "MIT",
17
+ "private": false,
18
+ "scripts": {
19
+ "dev": " NODE_ENV=development tsc --watch",
20
+ "build": "NODE_ENV=production tsc -p tsconfig.prod.json",
21
+ "prepublishOnly": "rm -rf ./dist && yarn build",
22
+ "lint": "tsc --noemit && eslint --ext '.ts' --ignore-path .gitignore .",
23
+ "test": "jest",
24
+ "benchmark": " node --loader ts-node/esm --experimental-specifier-resolution=node tests/benchmarks/benchmark.ts",
25
+ "profile": " node --loader ts-node/esm --experimental-specifier-resolution=node --prof --no-logfile-per-isolate tests/benchmarks/profile.ts && node --prof-process v8.log > v8.txt",
26
+ "playground": "node --loader ts-node/esm --experimental-specifier-resolution=node tests/playground/run.ts | tee tests/playground/index.html",
27
+ "serve": "python3 -m http.server --directory tests/playground 8080"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/core": "^7.12.10",
31
+ "@babel/plugin-transform-runtime": "^7.14.5",
32
+ "@babel/preset-env": "^7.12.11",
33
+ "@babel/preset-typescript": "^7.12.7",
34
+ "@babel/register": "^7.13.14",
35
+ "@bbob/html": "^2.8.1",
36
+ "@bbob/preset-html5": "^2.8.1",
37
+ "@thoughtsunificator/bbcode-parser-template": "^1.0.9",
38
+ "@types/benchmark": "^2.1.1",
39
+ "@types/jest": "^28.1.3",
40
+ "@types/markdown-it": "^12.2.3",
41
+ "@types/node": "^14",
42
+ "@typescript-eslint/eslint-plugin": "^5.23.0",
43
+ "@typescript-eslint/parser": "^5.23.0",
44
+ "babel-plugin-root-import": "^6.6.0",
45
+ "babel-plugin-transform-define": "^2.0.0",
46
+ "bbcode": "^0.1.5",
47
+ "bbcode-parser": "^1.0.10",
48
+ "bbcodejs": "^0.0.4",
49
+ "benchmark": "^2.1.4",
50
+ "eslint": "^8.15.0",
51
+ "eslint-config-standard": "^17.0.0",
52
+ "eslint-import-resolver-typescript": "^3.1.1",
53
+ "eslint-plugin-import": "^2.22.0",
54
+ "eslint-plugin-n": "^15.2.0",
55
+ "eslint-plugin-node": "^11.1.0",
56
+ "eslint-plugin-promise": "^6.0.0",
57
+ "eslint-plugin-vue": "^9.1.1",
58
+ "jest": "^28.1.0",
59
+ "markdown-it": "^13.0.1",
60
+ "ts-bbcode-parser": "^1.0.4",
61
+ "ts-node": "^10.8.1",
62
+ "typescript": "^4.2.4",
63
+ "ya-bbcode": "^1.0.12"
64
+ },
65
+ "engines": {
66
+ "node": ">=14.16"
67
+ }
68
+ }
@@ -0,0 +1,15 @@
1
+ import { Generator } from './generator/Generator'
2
+ import { htmlTransforms } from './generator/transforms/htmlTransforms'
3
+ import { Lexer } from './lexer/Lexer'
4
+ import { Parser } from './parser/Parser'
5
+
6
+ export function generateHtml(input: string, transforms = htmlTransforms): string {
7
+ const lexer = new Lexer()
8
+ const tokens = lexer.tokenize(input)
9
+
10
+ const parser = new Parser(transforms)
11
+ const root = parser.parse(input, tokens)
12
+
13
+ const generator = new Generator(transforms)
14
+ return generator.generate(root)
15
+ }
@@ -0,0 +1,60 @@
1
+ import { AstNode, AstNodeType, RootNode } from '../parser/AstNode'
2
+ import { nodeIsType } from '../parser/nodeIsType'
3
+ import { htmlTransforms } from './transforms/htmlTransforms'
4
+ import type { Transform } from './transforms/Transform'
5
+
6
+ export class Generator {
7
+ transforms: ReadonlyMap<string, Transform>
8
+
9
+ constructor(transforms = htmlTransforms) {
10
+ this.transforms = new Map(transforms.map((transform) => [transform.name, transform]))
11
+ }
12
+
13
+ generate(root: RootNode): string {
14
+ const stringify = (node: AstNode): string => {
15
+ let output = ''
16
+
17
+ if (nodeIsType(node, AstNodeType.TagNode)) {
18
+ const tagName = node.tagName
19
+ const transform = this.transforms.get(tagName)
20
+ if (!transform) {
21
+ throw new Error(`Unrecognized bbcode ${node.tagName}`)
22
+ }
23
+
24
+ const renderedStartTag = transform.start(node)
25
+ const renderedEndTag = transform.end?.(node) ?? ''
26
+ const isInvalidTag = renderedStartTag === false
27
+
28
+ if (isInvalidTag) {
29
+ output += node.ogStartTag
30
+ } else {
31
+ output += renderedStartTag
32
+ }
33
+
34
+ if (!transform.skipChildren || isInvalidTag) {
35
+ for (const child of node.children) {
36
+ output += stringify(child)
37
+ }
38
+ }
39
+
40
+ if (isInvalidTag) {
41
+ output += node.ogEndTag
42
+ } else {
43
+ output += renderedEndTag
44
+ }
45
+ } else if (nodeIsType(node, AstNodeType.TextNode)) {
46
+ output += node.str
47
+ } else if (nodeIsType(node, AstNodeType.LinebreakNode)) {
48
+ output += '\n'
49
+ } else {
50
+ for (const child of node.children) {
51
+ output += stringify(child)
52
+ }
53
+ }
54
+
55
+ return output
56
+ }
57
+
58
+ return stringify(root)
59
+ }
60
+ }
@@ -0,0 +1,15 @@
1
+ import type { TagNode } from '../../parser/AstNode'
2
+
3
+ export interface Transform {
4
+ name: string
5
+
6
+ skipChildren?: boolean // Do not recursively render children nodes (e.g. [img]url[/url] should not render url)
7
+ isStandalone?: boolean // Only has StartTag e.g. "[hr]"
8
+ isLinebreakTerminated?: boolean // Ends by linebreak e.g. "[*] list entry\n"
9
+
10
+ // Returns false when the StartTag failed validation and original bbcode will be displayed as plaintext
11
+ start: (tagNode: TagNode) => string | false
12
+
13
+ // Some tags do not need closing tag e.g. standalones like [hr] or wrappers like [img]
14
+ end?: (tagNode: TagNode) => string
15
+ }
@@ -0,0 +1,205 @@
1
+ import { getTagImmediateAttrVal } from '../utils/getTagImmediateAttrVal'
2
+ import { getTagImmediateText } from '../utils/getTagImmediateText'
3
+ import { getWidthHeightAttr } from '../utils/getWidthHeightAttr'
4
+ import { isDangerousUrl } from '../utils/isDangerousUrl'
5
+ import { isOrderedList } from '../utils/isOrderedList'
6
+ import type { Transform } from './Transform'
7
+
8
+ export const htmlTransforms: ReadonlyArray<Transform> = [
9
+ {
10
+ name: 'b',
11
+ start: () => {
12
+ return '<strong>'
13
+ },
14
+ end: () => {
15
+ return '</strong>'
16
+ },
17
+ },
18
+ {
19
+ name: 'i',
20
+ start: () => {
21
+ return '<em>'
22
+ },
23
+ end: () => {
24
+ return '</em>'
25
+ },
26
+ },
27
+ {
28
+ name: 'u',
29
+ start: () => {
30
+ return '<ins>'
31
+ },
32
+ end: () => {
33
+ return '</ins>'
34
+ },
35
+ },
36
+ {
37
+ name: 's',
38
+ start: () => {
39
+ return '<del>'
40
+ },
41
+ end: () => {
42
+ return '</del>'
43
+ },
44
+ },
45
+ {
46
+ name: 'style',
47
+ start: (tagNode) => {
48
+ let style = ''
49
+
50
+ for (const child of tagNode.attributes) {
51
+ switch (child.key) {
52
+ case 'color': {
53
+ style += `color:${child.val};`
54
+ continue
55
+ }
56
+ case 'size': {
57
+ if (/^\d+$/.test(child.val)) {
58
+ style += `font-size:${child.val}%;` // When no units provided (i.e. just a number), then assume %
59
+ } else {
60
+ style += `font-size:${child.val};`
61
+ }
62
+ continue
63
+ }
64
+ }
65
+ }
66
+
67
+ return `<span style="${style}">`
68
+ },
69
+ end: () => {
70
+ return '</span>'
71
+ },
72
+ },
73
+ {
74
+ name: 'color',
75
+ start: (tagNode) => {
76
+ const color = getTagImmediateAttrVal(tagNode)
77
+ return `<span style="color:${color};">`
78
+ },
79
+ end: () => {
80
+ return '</span>'
81
+ },
82
+ },
83
+ {
84
+ name: 'hr',
85
+ isStandalone: true,
86
+ start: () => {
87
+ return '<hr />'
88
+ },
89
+ },
90
+ {
91
+ name: 'list',
92
+ start: (tagNode) => {
93
+ return isOrderedList(tagNode)
94
+ ? '<ol>'
95
+ : '<ul>'
96
+ },
97
+ end: (tagNode) => {
98
+ return isOrderedList(tagNode)
99
+ ? '</ol>'
100
+ : '</ul>'
101
+ },
102
+ },
103
+ {
104
+ name: '*',
105
+ isLinebreakTerminated: true,
106
+ start: () => {
107
+ return '<li>'
108
+ },
109
+ end: () => {
110
+ return '</li>'
111
+ },
112
+ },
113
+ {
114
+ name: 'img',
115
+ skipChildren: true,
116
+ start: (tagNode) => {
117
+ const src = getTagImmediateText(tagNode)
118
+ if (!src) {
119
+ return false
120
+ }
121
+
122
+ if (isDangerousUrl(src)) {
123
+ return false
124
+ }
125
+
126
+ const { width, height } = getWidthHeightAttr(tagNode)
127
+
128
+ let str = `<img src="${src}"`
129
+ if (width) {
130
+ str += ` width="${width}"`
131
+ }
132
+ if (height) {
133
+ str += ` height="${height}"`
134
+ }
135
+ str += '>'
136
+ return str
137
+ },
138
+ },
139
+ {
140
+ name: 'url',
141
+ start: (tagNode) => {
142
+ const href = getTagImmediateAttrVal(tagNode) ?? getTagImmediateText(tagNode)
143
+ if (!href) {
144
+ return false
145
+ }
146
+
147
+ if (isDangerousUrl(href)) {
148
+ return false
149
+ }
150
+
151
+ return `<a href="${href}">`
152
+ },
153
+ end: () => {
154
+ return '</a>'
155
+ },
156
+ },
157
+ {
158
+ name: 'quote',
159
+ start: (tagNode) => {
160
+ const author = getTagImmediateAttrVal(tagNode)
161
+ return author
162
+ ? `<blockquote><strong>${author}</strong>`
163
+ : '<blockquote>'
164
+ },
165
+ end: () => {
166
+ return '</blockquote>'
167
+ },
168
+ },
169
+ {
170
+ name: 'table',
171
+ start: () => {
172
+ return '<table>'
173
+ },
174
+ end: () => {
175
+ return '</table>'
176
+ },
177
+ },
178
+ {
179
+ name: 'tr',
180
+ start: () => {
181
+ return '<tr>'
182
+ },
183
+ end: () => {
184
+ return '</tr>'
185
+ },
186
+ },
187
+ {
188
+ name: 'td',
189
+ start: () => {
190
+ return '<td>'
191
+ },
192
+ end: () => {
193
+ return '</td>'
194
+ },
195
+ },
196
+ {
197
+ name: 'code',
198
+ start: () => {
199
+ return '<code>'
200
+ },
201
+ end: () => {
202
+ return '</code>'
203
+ },
204
+ },
205
+ ]
@@ -0,0 +1,21 @@
1
+ import type { TagNode } from '../../parser/AstNode'
2
+
3
+ /**
4
+ * Gets the text of the immediate attribute of the current TagNode
5
+ *
6
+ * [url=https://en.wikipedia.org]English Wikipedia[/url]
7
+ *
8
+ * TagNode [url]
9
+ * AttrNode VAL="https://en.wikipedia.org" (returns this string)
10
+ * TextNode "https://en.wikipedia.org"
11
+ * RootNode
12
+ * TextNode "English Wikipedia"
13
+ */
14
+ export function getTagImmediateAttrVal(tagNode: TagNode): string | undefined {
15
+ if (tagNode.attributes.length !== 1) {
16
+ return undefined
17
+ }
18
+
19
+ const attrNode = tagNode.attributes[0]
20
+ return attrNode.val
21
+ }
@@ -0,0 +1,33 @@
1
+ import { AstNodeType, TagNode } from '../../parser/AstNode'
2
+ import { nodeIsType } from '../../parser/nodeIsType'
3
+
4
+ /**
5
+ * Gets the text of the immediate descendant of the current TagNode
6
+ *
7
+ * [url]https://en.wikipedia.org[/url]
8
+ *
9
+ * TagNode [url]
10
+ * RootNode
11
+ * TextNode "https://en.wikipedia.org" (returns this string)
12
+ */
13
+ export function getTagImmediateText(tagNode: TagNode): string | undefined {
14
+ if (tagNode.children.length !== 1) {
15
+ return undefined
16
+ }
17
+
18
+ const child = tagNode.children[0]
19
+ if (!nodeIsType(child, AstNodeType.RootNode)) {
20
+ return undefined
21
+ }
22
+
23
+ if (child.children.length !== 1) {
24
+ return undefined
25
+ }
26
+
27
+ const textNode = child.children[0]
28
+ if (!nodeIsType(textNode, AstNodeType.TextNode)) {
29
+ return undefined
30
+ }
31
+
32
+ return textNode.str
33
+ }
@@ -0,0 +1,51 @@
1
+ import type { TagNode } from '../../parser/AstNode'
2
+
3
+ /**
4
+ * Gets the width/height attributes of the TagNode if they exist
5
+ *
6
+ * [img 500x300]https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png[/img]
7
+ *
8
+ * RootNode
9
+ * TagNode [img] (returns width:500, height:300)
10
+ * AttrNode VAL="500x300"
11
+ * TextNode " 500x300"
12
+ * RootNode
13
+ * TextNode "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png"
14
+ *
15
+ * [img width=500 height=300]https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png[/img]
16
+ *
17
+ * RootNode
18
+ * TagNode [img] (returns width:500, height:300)
19
+ * AttrNode KEY="width" VAL="500"
20
+ * TextNode " width"
21
+ * TextNode "500"
22
+ * AttrNode KEY="height" VAL="300"
23
+ * TextNode " height"
24
+ * TextNode "300"
25
+ * RootNode
26
+ * TextNode "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png
27
+ */
28
+ export function getWidthHeightAttr(tagNode: TagNode): { width?: string; height?: string } {
29
+ let width: string | undefined
30
+ let height: string | undefined
31
+
32
+ for (const child of tagNode.attributes) {
33
+ if (child.key === 'width') {
34
+ width = child.val
35
+ }
36
+ if (child.key === 'height') {
37
+ height = child.val
38
+ }
39
+
40
+ const matches = /(\d+)x(\d+)/.exec(child.val)
41
+ if (matches) {
42
+ width = matches[1]
43
+ height = matches[2]
44
+ }
45
+ }
46
+
47
+ return {
48
+ width,
49
+ height,
50
+ }
51
+ }
@@ -0,0 +1,17 @@
1
+ const dangerousUriRe = /^(vbscript|javascript|file|data):/
2
+ const safeDataUriRe = /^data:image\/(gif|png|jpeg|webp);/
3
+
4
+ export function isDangerousUrl(url: string): boolean {
5
+ const normalizedUrl = url.trim().toLowerCase()
6
+
7
+ if (!dangerousUriRe.test(normalizedUrl)) {
8
+ return false
9
+ }
10
+
11
+ // Only a subset of data uris are considered safe
12
+ if (safeDataUriRe.test(normalizedUrl)) {
13
+ return false
14
+ }
15
+
16
+ return true
17
+ }
@@ -0,0 +1,28 @@
1
+ import type { TagNode } from '../../parser/AstNode'
2
+
3
+ /**
4
+ * Determines if the StartTag has an attribute of "1" to indicate that it's an ordered list
5
+ *
6
+ * [list=1]
7
+ *
8
+ * TagNode [list]
9
+ * AttrNode VAL="1"
10
+ * TextNode "1"
11
+ * RootNode
12
+ * TagNode [*]
13
+ * RootNode
14
+ * TextNode "Entry 1"
15
+ * TagNode [*]
16
+ * RootNode
17
+ * TextNode "Entry 2"
18
+ */
19
+ export function isOrderedList(node: TagNode): boolean {
20
+ for (const child of node.attributes) {
21
+ const val = child.val
22
+ if (val === '1') {
23
+ return true
24
+ }
25
+ }
26
+
27
+ return false
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export * from './generateHtml'
2
+
3
+ export * from './lexer/Lexer'
4
+ export * from './lexer/Token'
5
+ export * from './lexer/TokenType'
6
+
7
+ export * from './parser/Parser'
8
+ export * from './parser/AstNode'
9
+ export * from './parser/nodeIsType'
10
+
11
+ export * from './generator/Generator'
12
+ export * from './generator/transforms/Transform'
13
+ export * from './generator/transforms/htmlTransforms'
14
+ export * from './generator/utils/getWidthHeightAttr'
15
+ export * from './generator/utils/getTagImmediateAttrVal'
16
+ export * from './generator/utils/getTagImmediateText'
17
+ export * from './generator/utils/isDangerousUrl'
18
+ export * from './generator/utils/isOrderedList'
@@ -0,0 +1,89 @@
1
+ import { symbolTable, TokenType } from './TokenType'
2
+ import type { Token } from './Token'
3
+
4
+ export class Lexer {
5
+ tokenize(input: Readonly<string>): Array<Token> {
6
+ const tokens = new Array<Token>()
7
+
8
+ const re = /\n|\[\/|\[(\w+|\*)|\]|=|&|<|>|'|"/g
9
+ let offset = 0
10
+
11
+ while (true) {
12
+ // Match until next symbol
13
+ const match = re.exec(input)
14
+ if (!match) {
15
+ break
16
+ }
17
+
18
+ // Everything between previous symbol and current symbol is treated as plaintext
19
+ //
20
+ // [...]plaintext[/...]
21
+ // | |
22
+ // offset match.index
23
+ // (new) offset
24
+ //
25
+ const length = match.index - offset
26
+ if (length > 0) {
27
+ tokens.push({
28
+ type: TokenType.STR,
29
+ offset,
30
+ length,
31
+ })
32
+ }
33
+
34
+ offset = match.index
35
+
36
+ // Only add BACKSLASH token if it's preceded by L_BRACKET
37
+ // In the regex '[/' takes precedence over '['
38
+ if (match[0] === '[/') {
39
+ tokens.push({
40
+ type: TokenType.L_BRACKET,
41
+ offset,
42
+ length: 1,
43
+ })
44
+ offset += 1
45
+
46
+ tokens.push({
47
+ type: TokenType.BACKSLASH,
48
+ offset,
49
+ length: 1,
50
+ })
51
+ offset += 1
52
+ } else if (match[0].startsWith('[')) {
53
+ tokens.push({
54
+ type: TokenType.L_BRACKET,
55
+ offset,
56
+ length: 1,
57
+ })
58
+ offset += 1
59
+
60
+ const length = match[0].length - 1
61
+ tokens.push({
62
+ type: TokenType.STR,
63
+ offset,
64
+ length,
65
+ })
66
+ offset += length
67
+ } else {
68
+ tokens.push({
69
+ type: symbolTable[match[0]] ?? TokenType.STR,
70
+ offset,
71
+ length: 1,
72
+ })
73
+ offset += 1
74
+ }
75
+ }
76
+
77
+ // Add any leftover non-symbol text
78
+ const length = input.length - offset
79
+ if (length > 0) {
80
+ tokens.push({
81
+ type: TokenType.STR,
82
+ offset,
83
+ length,
84
+ })
85
+ }
86
+
87
+ return tokens
88
+ }
89
+ }
@@ -0,0 +1,64 @@
1
+ import { TokenType } from './TokenType'
2
+
3
+ export interface Token {
4
+ type: TokenType
5
+ offset: number
6
+ length: number
7
+ }
8
+
9
+ export function stringifyTokens(ogText: string, tokens: ReadonlyArray<Token>): string {
10
+ let s = ''
11
+
12
+ for (const token of tokens) {
13
+ switch (token.type) {
14
+ case TokenType.STR: {
15
+ s += ogText.substring(token.offset, token.offset + token.length)
16
+ break
17
+ }
18
+ case TokenType.LINEBREAK: {
19
+ s += '\n'
20
+ break
21
+ }
22
+
23
+ case TokenType.L_BRACKET: {
24
+ s += '['
25
+ break
26
+ }
27
+ case TokenType.R_BRACKET: {
28
+ s += ']'
29
+ break
30
+ }
31
+ case TokenType.BACKSLASH: {
32
+ s += '/'
33
+ break
34
+ }
35
+ case TokenType.EQUALS: {
36
+ s += '='
37
+ break
38
+ }
39
+
40
+ case TokenType.XSS_AMP: {
41
+ s += '&amp;'
42
+ break
43
+ }
44
+ case TokenType.XSS_LT: {
45
+ s += '&lt;'
46
+ break
47
+ }
48
+ case TokenType.XSS_GT: {
49
+ s += '&gt;'
50
+ break
51
+ }
52
+ case TokenType.XSS_D_QUOTE: {
53
+ s += '&quot;'
54
+ break
55
+ }
56
+ case TokenType.XSS_S_QUOTE: {
57
+ s += '&#x27;'
58
+ break
59
+ }
60
+ }
61
+ }
62
+
63
+ return s
64
+ }