Initial Commit
jdarwood007

jdarwood007 commited on 2023-04-13 14:52:52
Showing 8 changed files, with 1644 additions and 0 deletions.

... ...
@@ -0,0 +1,11 @@
1
+# Force our line endings to be LF, even for Windows
2
+* text=auto
3
+
4
+# Set certain files to be binary
5
+*.png binary
6
+*.jpg binary
7
+*.gif binary
8
+*.tgz binary
9
+*.zip binary
10
+*.tar.gz binary
11
+*.ttf binary
... ...
@@ -0,0 +1,190 @@
1
+checks:
2
+    php:
3
+        variable_existence: true
4
+        use_statement_alias_conflict: true
5
+        unused_variables: true
6
+        unused_properties: true
7
+        unused_parameters: true
8
+        unused_methods: true
9
+        unreachable_code: true
10
+        switch_fallthrough_commented: true
11
+        simplify_boolean_return: true
12
+        return_doc_comments: true
13
+        return_doc_comment_if_not_inferrable: true
14
+        require_scope_for_methods: true
15
+        require_php_tag_first: true
16
+        remove_extra_empty_lines: true
17
+        property_assignments: true
18
+        precedence_mistakes: true
19
+        precedence_in_conditions: true
20
+        parse_doc_comments: true
21
+        parameter_non_unique: true
22
+        parameter_doc_comments: true
23
+        param_doc_comment_if_not_inferrable: true
24
+        overriding_private_members: true
25
+        no_trailing_whitespace: true
26
+        no_short_open_tag: true
27
+        no_property_on_interface: true
28
+        no_non_implemented_abstract_methods: true
29
+        no_short_method_names:
30
+            minimum: '3'
31
+        no_goto: true
32
+        no_error_suppression: true
33
+        no_debug_code: true
34
+        more_specific_types_in_doc_comments: true
35
+        missing_arguments: true
36
+        method_calls_on_non_object: true
37
+        instanceof_class_exists: true
38
+        foreach_traversable: true
39
+        fix_use_statements:
40
+            remove_unused: true
41
+            preserve_multiple: false
42
+            preserve_blanklines: false
43
+            order_alphabetically: false
44
+        fix_line_ending: true
45
+        fix_doc_comments: true
46
+        encourage_shallow_comparison: true
47
+        duplication: true
48
+        deprecated_code_usage: true
49
+        deadlock_detection_in_loops: true
50
+        code_rating: true
51
+        closure_use_not_conflicting: true
52
+        closure_use_modifiable: true
53
+        catch_class_exists: true
54
+        avoid_duplicate_types: true
55
+        avoid_closing_tag: false
56
+        assignment_of_null_return: true
57
+        argument_type_checks: true
58
+        no_long_variable_names:
59
+            maximum: '40'
60
+        no_short_variable_names:
61
+            minimum: '3'
62
+        phpunit_assertions: true
63
+        remove_php_closing_tag: false
64
+        no_mixed_inline_html: false
65
+        require_braces_around_control_structures: false
66
+        psr2_control_structure_declaration: false
67
+        avoid_superglobals: false
68
+        security_vulnerabilities: false
69
+        no_exit: false
70
+coding_style:
71
+    php:
72
+        indentation:
73
+            general:
74
+                use_tabs: true
75
+                size: 4
76
+            switch:
77
+                indent_case: true
78
+        spaces:
79
+            general:
80
+                linefeed_character: newline
81
+            before_parentheses:
82
+                function_declaration: false
83
+                closure_definition: false
84
+                function_call: false
85
+                if: true
86
+                for: true
87
+                while: true
88
+                switch: true
89
+                catch: true
90
+                array_initializer: false
91
+            around_operators:
92
+                assignment: true
93
+                logical: true
94
+                equality: true
95
+                relational: true
96
+                bitwise: true
97
+                additive: true
98
+                multiplicative: true
99
+                shift: true
100
+                unary_additive: false
101
+                concatenation: true
102
+                negation: false
103
+            before_left_brace:
104
+                class: true
105
+                function: true
106
+                if: true
107
+                else: true
108
+                for: true
109
+                while: true
110
+                do: true
111
+                switch: true
112
+                try: true
113
+                catch: true
114
+                finally: true
115
+            before_keywords:
116
+                else: true
117
+                while: true
118
+                catch: true
119
+                finally: true
120
+            within:
121
+                brackets: false
122
+                array_initializer: false
123
+                grouping: false
124
+                function_call: false
125
+                function_declaration: false
126
+                if: false
127
+                for: false
128
+                while: false
129
+                switch: false
130
+                catch: false
131
+                type_cast: false
132
+            ternary_operator:
133
+                before_condition: true
134
+                after_condition: true
135
+                before_alternative: true
136
+                after_alternative: true
137
+                in_short_version: false
138
+            other:
139
+                before_comma: false
140
+                after_comma: true
141
+                before_semicolon: false
142
+                after_semicolon: true
143
+                after_type_cast: true
144
+        braces:
145
+            classes_functions:
146
+                class: new-line
147
+                function: new-line
148
+                closure: new-line
149
+            if:
150
+                opening: new-line
151
+                always: false
152
+                else_on_new_line: true
153
+            for:
154
+                opening: new-line
155
+                always: false
156
+            while:
157
+                opening: new-line
158
+                always: false
159
+            do_while:
160
+                opening: undefined
161
+                always: true
162
+                while_on_new_line: true
163
+            switch:
164
+                opening: new-line
165
+            try:
166
+                opening: new-line
167
+                catch_on_new_line: true
168
+                finally_on_new_line: true
169
+        upper_lower_casing:
170
+            keywords:
171
+                general: lower
172
+            constants:
173
+                true_false_null: lower
174
+
175
+
176
+build:
177
+    nodes:
178
+        analysis:
179
+            tests:
180
+                override:
181
+                    - php-scrutinizer-run
182
+            dependencies:
183
+                after:
184
+                    - git clone https://github.com/WordPress/WordPress.git wordpress
185
+
186
+filter:
187
+    dependency_paths:
188
+        - wordpress/
189
+    excluded_paths:
190
+        - '*.min.js'
... ...
@@ -0,0 +1,29 @@
1
+BSD 3-Clause License
2
+
3
+Copyright (c) 2023, SleePy
4
+All rights reserved.
5
+
6
+Redistribution and use in source and binary forms, with or without
7
+modification, are permitted provided that the following conditions are met:
8
+
9
+1. Redistributions of source code must retain the above copyright notice, this
10
+   list of conditions and the following disclaimer.
11
+
12
+2. Redistributions in binary form must reproduce the above copyright notice,
13
+   this list of conditions and the following disclaimer in the documentation
14
+   and/or other materials provided with the distribution.
15
+
16
+3. Neither the name of the copyright holder nor the names of its
17
+   contributors may be used to endorse or promote products derived from
18
+   this software without specific prior written permission.
19
+
20
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
... ...
@@ -0,0 +1,1170 @@
1
+/*
2
+ * Simple way to handle translation using a C# like Format for variable replacements.  Used for i18n.
3
+*/
4
+String.prototype.format = function() {return [...arguments].reduce((rt,cv,ci) => rt.replace("{" + ci + "}", cv), this);};
5
+txt = [];
6
+
7
+/* Some variables we pass around to other functions*/
8
+usingArchive = 'zip';
9
+archiveData = null;
10
+instructions = [];
11
+
12
+/* Handles the event when we upload */
13
+async function uploadFileToJS(evt)
14
+{
15
+	console.debug('Processing Uploaded file');
16
+
17
+	// Disable the event listners, so we can grab the SMF version data and repopulate data without triggering changes.
18
+	disableVersionChangeListiners();
19
+	archiveData = null;
20
+	await fetchSmfVersions().then(populateSmfVersions);
21
+
22
+	// Reading the file data into something.
23
+	let reader = new FileReader();
24
+	reader.onload = async function(evt) {
25
+		console.debug('Waiting on File Reader');
26
+		if(evt.target.readyState != 2)
27
+			return;
28
+		if(evt.target.error) {
29
+			throw new Error ('Unable to read uploaded file');
30
+			return;
31
+		}
32
+
33
+		let header = '';
34
+		const arr = (new Uint8Array(evt.target.result)).subarray(0, 4);
35
+		for (let j = 0; j < arr.length; j++) {
36
+			header += arr[j].toString(16);
37
+		}
38
+
39
+		// Zip
40
+		if (header == '504b34')
41
+		{
42
+			console.debug('Sending to Zip Handler');
43
+			usingArchive = 'zip';
44
+
45
+			JSZip.loadAsync(evt.target.result)
46
+				.then(processZipFile).catch(function (e) {
47
+					document.getElementById('errorContainer').removeAttribute('hidden');
48
+					document.getElementById('errorContainer').innerHTML = e;
49
+				});
50
+		}
51
+		// Tar.gz
52
+		/*
53
+			1f8b80: Unix compressed file 
54
+			1f8b88: FAT/MS DOS file system
55
+		*/
56
+		else if (header == '1f8b80' || header == '1f8b88')
57
+		{
58
+			console.debug('Sending to Tgz Handler');
59
+			usingArchive = 'tgz';
60
+
61
+			// Can't do this the easy way..
62
+			const tgz = evt.target.result;
63
+			const inf = pako.inflate(tgz);
64
+			const ab = inf.buffer;
65
+			const ut = await untar(ab);
66
+			processTarGzFile(ut);
67
+		}
68
+		// 425a6839 == Tar.Bz2
69
+		else
70
+		{
71
+			console.debug('Unsure what file type we where handed', header);
72
+			throw new Error ('Invalid usingArchive');
73
+		}
74
+	};
75
+
76
+	// Send the received file data to our file reader.
77
+	await reader.readAsArrayBuffer(evt.target.files[0]);
78
+}
79
+
80
+/* Handles receiving a file from a url.  Due to browser security, needs to be local or have appropriate COORS headers to allow us to receive it */
81
+async function runParser(lvUrlFile)
82
+{
83
+	console.debug('Loading File', lvUrlFile);
84
+
85
+	// Disable the event listners, so we can grab the SMF version data and repopulate data without triggering changes.
86
+	disableVersionChangeListiners();
87
+	await fetchSmfVersions().then(populateSmfVersions);
88
+
89
+	if (lvUrlFile.endsWith('tar.gz') || lvUrlFile.endsWith('tgz'))
90
+	{
91
+		usingArchive = 'tgz';
92
+		console.debug('Smells like a tgz');
93
+		await fetch(lvUrlFile).then(res => res.arrayBuffer()) // Download gzipped tar file and get ArrayBuffer
94
+			.then(pako.inflate)			 // Decompress gzip using pako
95
+			.then(arr => arr.buffer)		// Get ArrayBuffer from the Uint8Array pako returns
96
+			.then(untar)					// Untar
97
+			.then(processTarGzFile);
98
+	}
99
+	else if (lvUrlFile.endsWith('zip'))
100
+	{
101
+		usingArchive = 'zip';
102
+		console.debug('Seems to be a zip');
103
+
104
+		await new JSZip.external.Promise(function (resolve, reject) {
105
+			JSZipUtils.getBinaryContent(lvUrlFile, function(err, data) {
106
+				if (err) {
107
+					reject(err);
108
+				} else {
109
+					resolve(data);
110
+				}
111
+			});
112
+		}).then(function (data) {
113
+			return JSZip.loadAsync(data);
114
+		}).then(await processZipFile).catch(function (e) {
115
+			console.debug('Error!', e);
116
+			document.getElementById('errorContainer').removeAttribute('hidden');
117
+			document.getElementById('errorContainer').innerHTML = e;
118
+		});
119
+	}
120
+	else
121
+		throw new Error ('Invalid usingArchive');
122
+}
123
+
124
+/* Adds our event listner to changing the SMF version */
125
+function addVersionChangeListiners()
126
+{
127
+	console.debug('Enable Version Change Listner');
128
+	document.getElementById('smfVersions').addEventListener('change', changeSmfVersion);
129
+}
130
+
131
+/* Disables our event listner to change the SMF Version */
132
+function disableVersionChangeListiners()
133
+{
134
+	console.debug('Disable Version Change Listner');
135
+	document.getElementById('smfVersions').removeEventListener('change', changeSmfVersion);
136
+}
137
+
138
+/* Handles all logic we need to do in order to procoess the zip file */
139
+async function processZipFile(zip)
140
+{
141
+	console.debug('Processing ZIP');
142
+	archiveData = zip;
143
+	await processFile();
144
+}
145
+
146
+/* Handles all logic we need to do in order to procoess the tar.gz file */
147
+async function processTarGzFile(tgz)
148
+{
149
+	console.debug('Processing TGZ');
150
+	archiveData = tgz;
151
+	await processFile();
152
+}
153
+
154
+/* This processes the file, we have generic handlers and some functions will handle treating the zip/tar data differently as needed*/
155
+async function processFile()
156
+{
157
+	// This extracts the info file from the root.  If its in a sub folder, it won't find it.
158
+	infoData = await fetchFileFromArchive('package-info.xml');
159
+
160
+	// Attempt to parse the info file as valid xml data.
161
+	console.debug('Attempting to parse package-info.xml into XML Object');
162
+	parsedInfo = parseXml(infoData);
163
+	if (typeof parsedInfo === 'undefined')
164
+		throw new Error ('Unable to parse package-info.xml');
165
+
166
+	// Try to find our package name
167
+	console.debug('Attempting to find Customization Name');
168
+	packageName = parsedInfo.getElementsByTagName('name')[0]?.innerHTML;
169
+
170
+	// Can't find it, do not go any further.
171
+	if (typeof packageName === 'undefined')
172
+		throw new Error ('Unable to find Customization Name');
173
+
174
+	// Set up showing some information about this.
175
+	setPackageName(packageName);
176
+	showParserContainer();
177
+
178
+	// Ensure our event listners are disable, change our version drop down to list the versions we match better.
179
+	console.debug('Locating possible versions');
180
+	disableVersionChangeListiners();
181
+	let possibleInstalls = parsedInfo.getElementsByTagName('install');
182
+	for(let i = 0; i < possibleInstalls.length; i++)
183
+		processInstallXml(possibleInstalls[i]);
184
+
185
+	// We can listen for events now.
186
+	addVersionChangeListiners();
187
+
188
+	// Trigger a version change, which should have the best and newest match for this package now.
189
+	document.getElementById('smfVersions').dispatchEvent(new Event('change'));
190
+}
191
+
192
+/* This function retrieves a file from the archive for use later */
193
+async function fetchFileFromArchive(fileName)
194
+{
195
+	if (usingArchive == 'zip')
196
+	{
197
+		// The zip handler treats them as objects, but we need to ensure we can find files regardless of case, so we find the match, to find the real file name.
198
+		let match = archiveData.files[Object.keys(archiveData.files).find(key => key.toLowerCase() === fileName.toLowerCase())];
199
+
200
+		// We can't continue without the file.
201
+		if (match == undefined || match == null)
202
+			throw new Error('Unable to find ' + fileName);
203
+
204
+		// Now extract the info-file contents.
205
+		realFileName = match.name;
206
+		fileData = await archiveData.file(realFileName).async('string');
207
+
208
+		return fileData;
209
+	}
210
+	else if (usingArchive == 'tgz')
211
+	{
212
+		// The tar handler treats them as arrays.  But we also need to handle case sensitivity, so find the index.
213
+		let index = archiveData.map(function(e) { return e.name.toLowerCase(); }).indexOf(fileName.toLowerCase()) ?? -1;
214
+
215
+		// We can't continue without the file.
216
+		if (index < 0 || !archiveData[index])
217
+			throw new Error('Unable to find ' + fileName);
218
+
219
+		// Get our file data from the index we have.
220
+		fileData = await archiveData[index].readAsString();
221
+
222
+		return fileData;
223
+	}
224
+	else
225
+		throw new Error ('Invalid usingArchive');
226
+}
227
+
228
+/* Fetch SMF version data from a API, store it in localStage */
229
+async function fetchSmfVersions()
230
+{
231
+	let smfVersions = [];
232
+	const now = new Date();
233
+
234
+	// Try the cache.
235
+	const cachedVersions = localStorage.getItem('smfVersions');
236
+	if (cachedVersions)
237
+	{
238
+		const item = JSON.parse(cachedVersions);
239
+
240
+		// Cache is valid.
241
+		if (item.expiry && now.getTime() < item.expiry)
242
+			return item.value;
243
+	}
244
+
245
+	console.debug('Fetching SMF Version API');
246
+	try
247
+	{
248
+		await fetch(SmfVersionApiURL, {
249
+			method:'GET',
250
+			headers: {
251
+				'Content-Type': 'application/json'
252
+			},
253
+		})
254
+			.then(res => res.json())
255
+			.then(out => smfVersions = out)
256
+			.catch(err => { throw err });
257
+
258
+		// We have not always been semantic versioning, lets clean it up.
259
+		smfVersions.data.forEach(function(data, index, theArray) {
260
+			theArray[index] = semanticVersion(data);
261
+		});
262
+
263
+		const item = {
264
+			value: smfVersions,
265
+			expiry: now.getTime() + 100 * 60 * 60,
266
+		}
267
+		localStorage.setItem('smfVersions', JSON.stringify(item))
268
+
269
+		return smfVersions;
270
+	}
271
+	catch
272
+	{
273
+		console.debug('Failed to fetch SMF Versions, attempt fallback');
274
+
275
+		const cachedVersions = localStorage.getItem('smfVersions');
276
+		const item = JSON.parse(cachedVersions) ?? null;
277
+
278
+		// We failed, but do we have a cache to fall back on?
279
+		if (item != null && item.value != null)
280
+			return item.value;
281
+		else
282
+			throw new Error('Unable to fetch SMF Versions');
283
+	}
284
+}
285
+
286
+/* Populate the SMF Versions drop down */
287
+function populateSmfVersions(json)
288
+{
289
+	console.debug('Populating SMF Versions');
290
+
291
+	if (typeof json === 'undefined' || typeof json.data === 'undefined')
292
+		throw new Error ('Missing SMF Versions');
293
+
294
+	disableVersionChangeListiners();
295
+
296
+	// Clean out the existing data, this is supposed to be faster than innterHTML = '';
297
+	let pf = document.getElementById('preferedVersions');
298
+	if (pf && pf.hasChildNodes)
299
+		while (pf.firstChild)
300
+			pf.removeChild(pf.firstChild);
301
+	let ot = document.getElementById('otherVersions');
302
+	if (ot && ot.hasChildNodes)
303
+		while (ot.firstChild)
304
+			ot.removeChild(ot.firstChild);
305
+
306
+	// Sort, reverse and then put into the DDL
307
+	json.data.sort(window.compareVersions.compareVersions).reverse().forEach(function (ver) {
308
+		var option = document.createElement("option");
309
+		option.text = ver;
310
+		option.value = ver;
311
+
312
+		ot.appendChild(option);
313
+	});
314
+
315
+	addVersionChangeListiners();
316
+}
317
+
318
+/* Wrapper for DomParser to return a XML object */
319
+function parseXml(xmlString)
320
+{
321
+	const parser = new DOMParser();
322
+	const xmlDoc = parser.parseFromString(xmlString,'text/xml');
323
+
324
+	if (!xmlDoc)
325
+		throw new Error('Unable to Parse XML data');
326
+	return xmlDoc;
327
+}
328
+
329
+/* Show the Parser container */
330
+function showParserContainer()
331
+{
332
+	console.debug('Showing the Parser Container');
333
+	document.getElementById('instructionsContainer').removeAttribute('hidden');
334
+	document.getElementById('smfVersions').removeAttribute('hidden');
335
+}
336
+
337
+/* Hide the Parser container */
338
+function hideParserContainer()
339
+{
340
+	console.debug('Hidding the Parser Container');
341
+	document.getElementById('instructionsContainer').setAttribute('hidden');
342
+	document.getElementById('smfVersions').setAttribute('hidden');
343
+}
344
+
345
+/* Set our page title with the package name */
346
+function setPackageName(packageName)
347
+{
348
+	console.debug('Set the Package Name', packageName);
349
+	document.getElementById('packageName').innerHTML = packageName;
350
+}
351
+
352
+/* Processes the install XML data for the "for" attributes, finds the SMF versions supported, updating our prefered versions */
353
+function processInstallXml(xml)
354
+{
355
+	console.debug('Processing Install XML data');
356
+	let setDefault = true;
357
+
358
+	// Find a "for".  It is valid to not have a for, as it implies it installs for any version of SMF.
359
+	const forVersions = xml.getAttribute('for');
360
+	if (forVersions)
361
+	{
362
+		// The list is comma separated.
363
+		const versions = forVersions.split(',');
364
+		for (let j = 0; j < versions.length; j++)
365
+		{
366
+			// Fidn the first and last version in the string provided, even if just a single version.
367
+			vr = findVersionRangeFromFor(versions[j]);
368
+
369
+			// The newest match will be moved to Prefered versions.
370
+			setPreferedVersion(vr.end, vr.start);
371
+
372
+			// If we have not set a default, do it now.
373
+			if (setDefault)
374
+			{
375
+				console.debug('Setting a default version');
376
+				const cn = document.getElementById('preferedVersions').getElementsByTagName('option')[0]?.value ?? null;
377
+
378
+				if (cn != null)
379
+					document.getElementById('smfVersions').value = cn;
380
+				setDefault = false;
381
+			}
382
+		}
383
+	}
384
+}
385
+
386
+/* Given a version string from a install for="", we find the newest and oldest verison in that range */
387
+function findVersionRangeFromFor(forString)
388
+{
389
+	// No version, lets simplify it and say it matches anything.
390
+	if (forString == '' || forString == null)
391
+	{
392
+		console.debug('No version specified from for');
393
+		return {
394
+			'start': '0.0.0-Alpha1',
395
+			'end': '99.99.99'
396
+		};
397
+	}
398
+
399
+	// Strip off 'SMF';
400
+	forString = forString.replace('SMF', '');
401
+
402
+	// No - nor *, must just be a single version, no other wildcards are supported.
403
+	if (forString.indexOf('-') == -1 && forString.indexOf('*') == -1)
404
+	{
405
+		console.debug('No Range, single version', forString);
406
+		return {
407
+			'start': semanticVersion(forString),
408
+			'end': semanticVersion(forString)
409
+		};
410
+	}
411
+
412
+	// If we have a * we need to split it up.  "SMF 2.1.*"
413
+	if (forString.indexOf('*') > -1)
414
+		forString = forString.replace('.*', '.0') + '-' + forString.replace('.*', '.99');
415
+
416
+	// Now we have for sure a range, split it.  "2.1.3-2.1.52"
417
+	const es = forString.split('-');
418
+
419
+	// SMF consideres '2.0' to be '2.0.0-Alpha1' not '2.0.0'
420
+	if (es[0].trim().match(/^\d\.\d$/, 'g'))
421
+		es[0] = es[0].trim().replace(/^(\d)\.(\d)$/, '$1.$2.0-Alpha1', 'g');
422
+
423
+	console.debug('Finding Version Range For', forString, es);
424
+	return {
425
+		'start': semanticVersion(es[0].trim()),
426
+		'end': semanticVersion(es[1].trim())
427
+	};
428
+}
429
+
430
+/* Given a newest and oldest version, try to find the best match and move only that one to the prefered versions */
431
+function setPreferedVersion(end, start)
432
+{
433
+	console.debug('Setting Prefered Versions', end, start);
434
+	const pf = document.getElementById('preferedVersions');
435
+	const ot = document.getElementById('otherVersions');
436
+	const otOpts = ot.getElementsByTagName('option');
437
+
438
+	for(let i = 0; i < otOpts.length; i++)
439
+	{
440
+		if (compareVersions.compare(otOpts[i].value, start, '>=') && compareVersions.compare(otOpts[i].value, end, '<='))
441
+		{
442
+			pf.appendChild(otOpts[i]);
443
+
444
+			// Because we moved the node, move the counter back one.  This is only really needed if we want to multi-match.
445
+			--i;
446
+
447
+			break;
448
+		}
449
+	}
450
+}
451
+
452
+/* When we change the SMF Version DDL, we trigger redoing parser data.  This is also triggered when the package is first uploaded */
453
+async function changeSmfVersion(e)
454
+{
455
+	let installXml = null;
456
+	const selectedVersion = document.getElementById('smfVersions').value;
457
+
458
+	console.debug('Changing SMF Version', selectedVersion);
459
+
460
+	const possibleInstalls = parsedInfo.getElementsByTagName('install');
461
+	for (let i = 0; i < possibleInstalls.length; i++)
462
+	{
463
+		const forVersions = possibleInstalls[i].getAttribute('for');
464
+
465
+		const versions = forVersions.split(',');
466
+		for (let j = 0; j < versions.length; j++)
467
+		{
468
+			vr = findVersionRangeFromFor(versions[j]);
469
+
470
+			if (compareVersions.compare(selectedVersion, vr.start, '>=') && compareVersions.compare(selectedVersion, vr.end, '<='))
471
+			{
472
+				installXml = i;
473
+				break;
474
+			}
475
+		}
476
+
477
+		if (installXml != null)
478
+			break;
479
+	}
480
+
481
+	if (installXml == null)
482
+		throw new Error ('No valid Install instructions for this SMF version found');
483
+
484
+	// Set the install xml we want to use and parse it.
485
+	const seletedInstallNode = possibleInstalls[installXml];
486
+	await parseInstallNode(seletedInstallNode);
487
+}
488
+
489
+/* Process a install node for actions to be taken */
490
+async function parseInstallNode(installNode)
491
+{
492
+	if (!installNode || !installNode.children)
493
+		throw new Error ('Invalid Install instructions');
494
+
495
+	// Clear this out incase it wasn't.
496
+	instructions = [];
497
+
498
+	for (let i = 0; i < installNode.children.length; i++) {
499
+		let thisNode = installNode.children[i];
500
+
501
+		switch (thisNode.tagName)
502
+		{
503
+			case 'readme':
504
+				if (!instructions['readme'])
505
+					instructions['readme'] = [];
506
+				instructions['readme'].push(await parseReadmeNode(thisNode));
507
+				break;
508
+
509
+			// Code and database are essentially the same, with the exception of database operations during package install are tracked.
510
+			case 'code':
511
+			case 'database':
512
+				if (!instructions['code'])
513
+					instructions['code'] = [];
514
+				instructions['code'].push(await parseCodeNode(thisNode));
515
+				break;
516
+
517
+			case 'create-dir':
518
+			case 'create-file':
519
+			case 'require-dir':
520
+			case 'require-file':
521
+			case 'move-dir':
522
+			case 'move-file':
523
+			case 'remove-dir':
524
+			case 'remove-file':
525
+				if (!instructions['fileop'])
526
+					instructions['fileop'] = [];
527
+				instructions['fileop'].push(await parseFileOpNode(thisNode, thisNode.tagName));
528
+				break;
529
+
530
+			case 'hook':
531
+				if (!instructions['hook'])
532
+					instructions['hook'] = [];
533
+				instructions['hook'].push(await parseHookNode(thisNode));
534
+				break;
535
+
536
+			case 'credits':
537
+				if (!instructions['credits'])
538
+					instructions['credits'] = [];
539
+				instructions['credits'].push(await parseCreditNode(thisNode));
540
+				break;
541
+
542
+			case 'modification':
543
+				if (!instructions['modification'])
544
+					instructions['modification'] = [];
545
+				instructions['modification'].push(await parseModificationNode(thisNode));
546
+				break;
547
+		}
548
+	}
549
+
550
+	// We have build all the data, render it on the page.
551
+	buildPage();
552
+}
553
+
554
+/* Takes a <readme> node and parses out the data */
555
+async function parseReadmeNode(node)
556
+{
557
+	let lang = 'english';
558
+	let text = '';
559
+
560
+	if (node.getAttribute('lang'))
561
+		lang = node.getAttribute('lang');
562
+
563
+	if (node.getAttribute('type') && node.getAttribute('type') == 'inline')
564
+		text = node.innerHTML;
565
+	// We have a file.
566
+	else
567
+	{
568
+		const fileName = node.innerHTML.trim();
569
+		text = await fetchFileFromArchive(fileName);
570
+	}
571
+
572
+	if (node.getAttribute('parsebbc') && node.getAttribute('parsebbc') === 'true')
573
+		text = bbcParser.parse(text);
574
+
575
+	return {
576
+		lang: lang,
577
+		text: text
578
+	};
579
+}
580
+
581
+/* Takes a <code> node and parses out the data */
582
+async function parseCodeNode(node)
583
+{
584
+	let code = '';
585
+	let fileName = '';
586
+
587
+	if (node.getAttribute('type') && node.getAttribute('type') == 'inline')
588
+	{
589
+		fileName = 'inlineCode.php';
590
+		code = node.innerHTML;
591
+	}
592
+	// We have a file.
593
+	else
594
+	{
595
+		fileName = node.innerHTML.trim();
596
+		code = await fetchFileFromArchive(fileName);
597
+	}
598
+
599
+	return {
600
+		fileName: fileName,
601
+		code: code
602
+	};
603
+}
604
+
605
+/* Takes various file based operation nodes and parses out the data */
606
+async function parseFileOpNode(node, op)
607
+{
608
+	return {
609
+		type: op,
610
+		name: node.getAttribute('name') ?? '',
611
+		destination: node.getAttribute('destination') ?? '',
612
+		from: node.getAttribute('from') ?? ''
613
+	};
614
+}
615
+
616
+/* Takes a <hook> node and parses out the data */
617
+async function parseHookNode(node)
618
+{
619
+	return {
620
+		func: node.getAttribute('function'),
621
+		name: node.getAttribute('hook') ?? node.innerHTML,
622
+		include_file: node.getAttribute('file'),
623
+		reverse: node.getAttribute('reverse') && node.getAttribute('reverse') == 'true' ? true : false,
624
+		object: node.getAttribute('object') && node.getAttribute('object') == 'true' ? true : false
625
+	};
626
+}
627
+
628
+/* Takes a <credit> node and parses out the data */
629
+async function parseCreditNode(node)
630
+{
631
+	return {
632
+		title: node.innerHTML,
633
+		url: node.getAttribute('url') ?? '',
634
+		license: node.getAttribute('license') ?? '',
635
+		licenseurl: node.getAttribute('licenseurl') ?? '',
636
+		copyright: node.getAttribute('copyright') ?? '',
637
+		version: parsedInfo.getElementsByTagName('version')[0]?.innerHTML ?? ''
638
+	};
639
+}
640
+
641
+/* Takes a <modification> node and parses out the data */
642
+async function parseModificationNode(node)
643
+{
644
+	let edits = null;
645
+	let reverse = false;
646
+	const fileName = node.innerHTML.trim();
647
+	fileContents = await fetchFileFromArchive(fileName);
648
+
649
+	if (node.getAttribute('reverse') && node.getAttribute('reverse') == 'true')
650
+		reverse = true;
651
+
652
+	// Only other format supported is boardmod.
653
+	if (node.getAttribute('format') && node.getAttribute('format') == 'boardmod')
654
+	{
655
+		console.debug('BoardMod detected');
656
+		edits = parseBoardBoard(fileContents);
657
+	}
658
+	else
659
+	{
660
+		console.debug('Modification XML detected');
661
+		edits = parseModificationXML(fileContents);
662
+	}
663
+
664
+	return {
665
+		edits: edits,
666
+		reverse: reverse,
667
+	};
668
+}
669
+
670
+/* BoardMod is not used anymore, but its still supported */
671
+function parseBoardBoard(data)
672
+{
673
+	let edits = [];
674
+	let file = '';
675
+	let search = '';
676
+	let position = '';
677
+	let add = '';
678
+
679
+	// Match our board mod data.
680
+	const matches = data.matchAll(/<(edit file|file|search|search for|add|add after|replace|add before|add above|above|before|below)>\n?(.*?)\n?<\/\1>/gims);
681
+	for (const match of matches)
682
+	{
683
+		// Update our file data.
684
+		if (match[1] == 'file' || match[1] == 'edit file')
685
+		{
686
+			file = match[2];
687
+		}
688
+		// Found a search data.
689
+		else if (file != '' && (match[1] == 'search' || match[1] == 'search for'))
690
+		{
691
+			search = match[2];
692
+		}
693
+		// If we have file and search data, we can now match for the add/edit data.
694
+		else if (file != '' && search != '')
695
+		{
696
+			// If its 'add before', 'before', 'add above' or 'above' means the add/edit code should come after.
697
+			if (match[1].includes('before') || match[1].includes('above'))
698
+				position = 'after';
699
+			// If its 'add after', or 'below', the add/edit code should come before.
700
+			else if (match[1].includes('after') || match[1].includes('below'))
701
+				position = 'before';
702
+			// If its 'add', 'replace', we are replacing the code.
703
+			else
704
+				position = 'replace';
705
+
706
+			edits.push({
707
+				file: file,
708
+				search: search,
709
+				position: position,
710
+				add: match[2],
711
+				fileSkipOnError: false,
712
+				opSkipOnError: false
713
+			});
714
+
715
+			// Reset our search.
716
+			search = '';
717
+		}
718
+	}
719
+
720
+	return edits;
721
+}
722
+
723
+/* Parse Modification XML data */
724
+function parseModificationXML(data)
725
+{
726
+	let edits = [];
727
+	let file = '';
728
+	let search = '';
729
+	let position = '';
730
+	let add = '';
731
+	let fileSkipOnError = false;
732
+	let opSkipOnError = false;
733
+	let opUseRegex = false;
734
+	let opSearchEOF = false;
735
+
736
+	const parser = new DOMParser();
737
+	const xmlDoc = parser.parseFromString(data,'text/xml');
738
+
739
+	if (!xmlDoc)
740
+		throw new Error('Unable to Parse XML data');
741
+
742
+	let fileEdits = xmlDoc.getElementsByTagName('file');
743
+	for (let i = 0; i < fileEdits.length; i++)
744
+	{
745
+		// Firstly find our file information.
746
+		file = fileEdits[i].getAttribute('name');
747
+
748
+		// When the file has error="ignore" or error="skip", we are able to skip any errors locating changes for the entire file.
749
+		fileSkipOnError = (fileEdits[i].getAttribute('error') == 'ignore' || fileEdits[i].getAttribute('error') == 'skip');
750
+
751
+		// No edits? Skip.
752
+		if (fileEdits[i].getElementsByTagName('operation').length == 0)
753
+			continue;
754
+
755
+		let ops = fileEdits[i].getElementsByTagName('operation');
756
+		for (let j = 0; j < ops.length; j++)
757
+		{
758
+			position = ops[j].getElementsByTagName('search')[0]?.getAttribute('position');
759
+
760
+			// When the operation has error="ignore" or error="skip", we are able to skip any errors locating changes for just this operation.
761
+			opSkipOnError = (ops[j].getAttribute('error') == 'ignore' || ops[j].getAttribute('error') == 'skip');
762
+
763
+			// Regex is supported for operations.  However it is not reversed.
764
+			opUseRegex = ops[j].getElementsByTagName('search')[0]?.getAttribute('regexp') == 'true';
765
+			add = HtmlEncode(ops[j].getElementsByTagName('add')[0]?.childNodes[0].data);
766
+
767
+			// If the position is end, pretend its looking for the PHP closing tag to make it easier'
768
+			search = position == 'end' ? '?' + '>' : HtmlEncode(ops[j].getElementsByTagName('search')[0]?.childNodes[0].data);
769
+			opSearchEOF = position == 'end' ? true : false;
770
+
771
+			/* We add a edit only if
772
+				1. Any of the following conditions. (OR)
773
+					a. search string is not empty
774
+					b. We are looking for the end of the file
775
+				2. We have a valid position.
776
+				3. We have a non empty add/edit string.
777
+			*/
778
+			if ((search.length > 0 || opSearchEOF) && position.length > 0 && add.length > 0)
779
+				edits.push({
780
+				file: file,
781
+				search: search,
782
+				position: position,
783
+				add: add,
784
+				fileSkipOnError: fileSkipOnError,
785
+				opSkipOnError: opSkipOnError,
786
+				opUseRegex: opUseRegex,
787
+				opSearchEOF : opSearchEOF
788
+			});
789
+		}
790
+	}
791
+
792
+	return edits;
793
+}
794
+
795
+/* Take all of the data we have built and build HTML output */
796
+function buildPage()
797
+{
798
+	console.debug('Building Page');
799
+
800
+	// Clean it out first, this is faster than innerHTML = '' according to the internet.
801
+	let ic = document.getElementById('instructionsContainer');
802
+	if (ic && ic.hasChildNodes)
803
+		while (ic.firstChild)
804
+			ic.removeChild(ic.firstChild);
805
+
806
+	/*
807
+		The Order of Operations on the output are:
808
+			Readme
809
+			Modifications
810
+			Hooks
811
+			Code/Database
812
+			File Operations
813
+			Credits
814
+		*/
815
+
816
+	if (instructions.readme != null && instructions.readme.length > 0)
817
+		buildPageReadme();
818
+
819
+	if (instructions.modification != null && instructions.modification.length > 0)
820
+		buildPageModifications();
821
+
822
+	if (instructions.hook != null && instructions.hook.length > 0)
823
+		buildPageHooks();
824
+
825
+	if (instructions.code != null && instructions.code.length > 0)
826
+		buildPageCode();
827
+
828
+	if (instructions.fileop != null && instructions.fileop.length > 0)
829
+		buildPageFileOperations();
830
+
831
+	if (instructions.credits != null && instructions.credits.length > 0)
832
+		buildPageCredits();
833
+
834
+	// Leave this debugging info here, useful to know and call when working on it.
835
+	//console.debug('instructions', instructions, instructions.modification[0].edits);
836
+}
837
+
838
+/* Prepare output for a readme section */
839
+function buildPageReadme()
840
+{
841
+	const langs = instructions.readme.map(function(e) {return e.lang;});
842
+	const lang = 'english';
843
+
844
+	const index = instructions.readme.map(function(e) { return e.lang.toLowerCase(); }).indexOf(lang.toLowerCase()) ?? -1;
845
+	const thisReadme = instructions.readme[index];
846
+
847
+	let template = document.getElementById('templateReadme').cloneNode(true);
848
+	template.removeAttribute('hidden');
849
+	template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_readme']).replace('{CONTENT}', thisReadme.text);
850
+
851
+	document.getElementById('instructionsContainer').appendChild(template);
852
+}
853
+
854
+/* Prepare output for a modification section */
855
+function buildPageModifications()
856
+{
857
+	let templateTitle = document.getElementById('templateOperationsTitle').cloneNode(true);
858
+	let fileName = '';
859
+	let templateFileTitle = null;
860
+	let template = null;
861
+
862
+	// Setup the title.
863
+	templateTitle.removeAttribute('id');
864
+	templateTitle.removeAttribute('hidden');
865
+	templateTitle.innerHTML = templateTitle.innerHTML.replace('{TITLE}', txt['title_operations']);
866
+	document.getElementById('instructionsContainer').appendChild(templateTitle);
867
+
868
+	// Work through all modifications we do, which could be from multiple modification files.
869
+	for (let i = 0; i < instructions.modification.length; i++)
870
+	{
871
+		const thisOperation = instructions.modification[i];
872
+
873
+		// Loop through all edits we do.
874
+		for (let j = 0; j < thisOperation.edits.length; j++)
875
+		{
876
+			const thisEdit = thisOperation.edits[j];
877
+
878
+			// Set a new file name.
879
+			if (fileName != thisEdit.file)
880
+			{
881
+				fileName = thisEdit.file;
882
+				templateFileTitle = document.getElementById('templateOperationsTitle').cloneNode(true);
883
+				templateFileTitle.removeAttribute('id');
884
+				templateFileTitle.removeAttribute('hidden');
885
+				templateFileTitle.innerHTML = templateFileTitle.innerHTML.replace('{TITLE}', fileName);
886
+				document.getElementById('instructionsContainer').appendChild(templateFileTitle);
887
+
888
+				if (thisEdit.fileSkipOnError == true)
889
+				{
890
+					templateFileTitle.querySelector('.alert').innerHTML = txt['file_edit_skip_error'];
891
+					templateFileTitle.querySelector('.alert').removeAttribute('hidden');
892
+				}
893
+			}
894
+
895
+			// Start working on our template.
896
+			template = document.getElementById('templateOperations').cloneNode(true);
897
+			template.removeAttribute('id');
898
+			template.removeAttribute('hidden');
899
+
900
+			// The search section.
901
+			template.querySelector('.searchContainer h5').innerHTML = thisEdit.position == 'end' ? txt['operation_end'] : txt['operation_search'];
902
+
903
+			// If we are going in reverse, flip some logic around.
904
+			if (thisOperation.reverse != null && thisOperation.reverse == true)
905
+			{
906
+				if (thisEdit.position == 'before')
907
+					thisEdit.position = 'after';
908
+				else if (thisEdit.position == 'after')
909
+					thisEdit.position = 'before';
910
+
911
+				template.querySelector('.addContainer h5').innerHTML = txt['operation_' + thisEdit.position];
912
+				template.querySelector('.operationSearch pre').innerHTML = thisEdit.add;
913
+				template.querySelector('.operationAdd pre').innerHTML = thisEdit.search;
914
+			}
915
+			else
916
+			{
917
+				template.querySelector('.addContainer h5').innerHTML = thisEdit.position == 'end' ? txt['operation_before'] : txt['operation_' + thisEdit.position];
918
+				template.querySelector('.operationSearch pre').innerHTML = thisEdit.search;
919
+				template.querySelector('.operationAdd pre').innerHTML = thisEdit.add;
920
+			}
921
+
922
+			// If we can skip this operation, add a notice.
923
+			if (thisEdit.opSkipOnError == true)
924
+			{
925
+				template.querySelector('.alert.alert-warning').innerHTML = txt['edit_skip_error'];
926
+				template.querySelector('.alert.alert-warning').removeAttribute('hidden');
927
+			}
928
+
929
+			// If this is a regex search, they will need something better than notepad.
930
+			if (thisEdit.opUseRegex == true)
931
+			{
932
+				template.querySelector('.alert.alert-info').innerHTML = txt['edit_uses_regex'];
933
+				template.querySelector('.alert.alert-info').removeAttribute('hidden');
934
+			}
935
+
936
+			document.getElementById('instructionsContainer').appendChild(template);
937
+		}
938
+	}
939
+
940
+	// Make it so we can click the clipboard icon and copy the data.
941
+	document.querySelectorAll('.operationSearchCopy,.operationAddCopy').forEach(el => {
942
+		el.addEventListener('click', function(evt) {
943
+			const CodeArea = this.parentNode.nextSibling.nextSibling.querySelector('pre');
944
+			const CurSelection = window.getSelection();
945
+
946
+			// Webkit based browsers support setBaseAndExtent.
947
+			if (CurSelection.setBaseAndExtent)
948
+			{
949
+				CurSelection.setBaseAndExtent(CodeArea, 0, CodeArea, CodeArea.childNodes.length);
950
+			}
951
+			// Firefox and others.
952
+			else
953
+			{
954
+				const curRange = document.createRange();
955
+				curRange.selectNodeContents(CodeArea);
956
+				CurSelection.removeAllRanges();
957
+				CurSelection.addRange(curRange);
958
+			}
959
+
960
+			// Try to execute the copy command.  Give up silently if it doesn't.
961
+			try {
962
+				document.execCommand('copy');
963
+			} catch (err) {
964
+			}
965
+		});
966
+	});
967
+}
968
+
969
+/* Prepare output for a hooks section */
970
+function buildPageHooks()
971
+{
972
+	let template = document.getElementById('templateHooks').cloneNode(true);
973
+	template.removeAttribute('hidden');
974
+
975
+	// Loop through all data and just put it into a nice table.
976
+	for (let i = 0; i < instructions.hook.length; i++)
977
+	{
978
+		const thisHook = instructions.hook[i];
979
+
980
+		let newTR = document.createElement('tr');
981
+		let newTD = document.createElement('td');
982
+		newTR.appendChild(newTD);
983
+
984
+		if (thisHook.name == 'integrate_pre_include')
985
+			newTD.innerHTML = txt['hook_pre_include'].format(thisHook.func);
986
+		else
987
+			newTD.innerHTML = txt['hook_add'].format(thisHook.func, thisHook.name);
988
+
989
+		template.querySelector('table tbody').appendChild(newTR);
990
+	}
991
+
992
+	template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_hook']);
993
+	document.getElementById('instructionsContainer').appendChild(template);
994
+}
995
+
996
+/* Prepare output for a code section */
997
+function buildPageCode()
998
+{
999
+	let template = document.getElementById('templateCode').cloneNode(true);
1000
+	template.removeAttribute('hidden');
1001
+
1002
+	// Loop through all data and just put it into a nice table.
1003
+	for (let i = 0; i < instructions.code.length; i++)
1004
+	{
1005
+		const thisCode = instructions.code[i];
1006
+
1007
+		let newTR = document.createElement('tr');
1008
+		let newTD = document.createElement('td');
1009
+		newTR.appendChild(newTD);
1010
+
1011
+		newTD.innerHTML = thisCode.fileName;
1012
+
1013
+		// Build a container and link to download the code.
1014
+		let downloadTD = document.createElement('td');
1015
+		newTR.appendChild(downloadTD);
1016
+		downloadAnchor = document.createElement('a');
1017
+		downloadAnchor.innerHTML = txt['download'];
1018
+		downloadAnchor.download = thisCode.fileName;
1019
+
1020
+		const file = new Blob([Uint8Array.from(thisCode.code, c => c.charCodeAt(0))])
1021
+		const url = URL.createObjectURL(file)
1022
+		downloadAnchor.href = url;
1023
+
1024
+		downloadTD.appendChild(downloadAnchor);
1025
+
1026
+		template.querySelector('table tbody').appendChild(newTR);
1027
+	}
1028
+
1029
+	template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_code']);
1030
+	document.getElementById('instructionsContainer').appendChild(template);
1031
+}
1032
+
1033
+/* Prepare output for file operations section */
1034
+function buildPageFileOperations()
1035
+{
1036
+	let template = document.getElementById('templateFileOperations').cloneNode(true);
1037
+	template.removeAttribute('hidden');
1038
+
1039
+	// Loop through all data and just put it into a nice table.
1040
+	for (let i = 0; i < instructions.fileop.length; i++)
1041
+	{
1042
+		const thisHook = instructions.fileop[i];
1043
+
1044
+		let newTR = document.createElement('tr');
1045
+		let newTD = document.createElement('td');
1046
+		newTR.appendChild(newTD);
1047
+
1048
+		let msg = txt['file_operation_' + thisHook.type];
1049
+		if (thisHook.name != null && thisHook.destination != null)
1050
+			msg = msg.format(thisHook.name, formatPath(thisHook.destination));
1051
+		else if (thisHook.name != null)
1052
+			msg = msg.format(thisHook.name);
1053
+
1054
+		newTD.innerHTML = msg;
1055
+		template.querySelector('table tbody').appendChild(newTR);
1056
+	}
1057
+
1058
+	template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_fileop']);
1059
+	document.getElementById('instructionsContainer').appendChild(template);
1060
+}
1061
+
1062
+/* Prepare output for credits section, SMF would normally insert this into a table to track. */
1063
+function buildPageCredits()
1064
+{
1065
+	let template = document.getElementById('templateCredits').cloneNode(true);
1066
+	template.removeAttribute('hidden');
1067
+
1068
+	// Loop through all data and just put it into a nice table.
1069
+	for (let i = 0; i < instructions.credits.length; i++)
1070
+	{
1071
+		const thisCredit = instructions.credits[i];
1072
+
1073
+		let newTR = document.createElement('tr');
1074
+		let newTD = document.createElement('td');
1075
+		newTR.appendChild(newTD);
1076
+
1077
+		let credit = '';
1078
+		if (thisCredit.url != null)
1079
+			credit = '<a href=' + thisCredit.url + '" rel="noopener">' + (thisCredit.title ?? packageName) + ': ' + txt['credits_version'] + ' ' + thisCredit.version + '</a>';
1080
+		else
1081
+			credit = (thisCredit.title ?? packageName) + ': ' + txt['credits_version'] + ' ' + thisCredit.version;
1082
+
1083
+		if (thisCredit.licenseurl != null)
1084
+			credit += ' | ' + txt['credits_license'] + '<a href="' + thisCredit.licenseurl + '">' + thisCredit.license + '</a>';
1085
+		else
1086
+			credit += ' | ' + txt['credits_license'] + thisCredit.license;
1087
+
1088
+		newTD.innerHTML = credit;
1089
+		template.querySelector('table tbody').appendChild(newTR);
1090
+	}
1091
+
1092
+	template.innerHTML = template.innerHTML.replace('{TITLE}', txt['title_credits']);
1093
+	document.getElementById('instructionsContainer').appendChild(template);
1094
+}
1095
+
1096
+/* SMF hasn't been consistent nor always following semantic versioning.  So do some cleanup to help with issues parsing versions */
1097
+function semanticVersion(version)
1098
+{
1099
+	// Straight forward simple replacements.
1100
+	const replacements = [
1101
+		{k: ' Security Patch', v:'.1'} // 2.0 RC4 Security Patch
1102
+	];
1103
+
1104
+	// Some regular expression replacements.
1105
+	const pregReplacments = [
1106
+		// 2.0 => 2.0.0
1107
+		{k: /^(\d)\.(\d)$/, v:'$1.$2.0'},
1108
+
1109
+		// 2.0 RC-1 => 2.0.0-RC1
1110
+		{k: /^(\d)\.(\d) RC(\s-)?(\d)$/, v:'$1.$2.0-RC$4'},
1111
+
1112
+		// 2.0 RC 2-1 => 2.0.0-RC2.1
1113
+		{k: /^(\d)\.(\d) RC(\s-)?(\d)[\.|-](\d)$/, v:'$1.$2.0-RC$4.$5'},
1114
+
1115
+		// 2.1 Beta 3 Public => 2.1.0-Beta3
1116
+		{k: /^(\d)\.(\d) Beta\s?(\d)( Public)?$/, v:'$1.$2.0-Beta$3'},
1117
+
1118
+		// 2.1 Beta 2.1 => 2.1.0-Beta2.1
1119
+		{k: /^(\d)\.(\d) Beta\s?(\d)[\.|-](\d)( Public)?$/, v:'$1.$2.0-Beta$3.$4'}
1120
+	];
1121
+
1122
+	// Do the simple ones first.
1123
+	version = replacements.reduce((rt,cv,ci) => rt.replace(cv['k'],cv['v']), version);
1124
+
1125
+	// Do our regular expressions.
1126
+	version = pregReplacments.reduce(function (rt,cv,ci) {
1127
+		let re = new RegExp(cv['k'], 'gi');
1128
+		return rt.replace(re, cv['v']);
1129
+	}, version);
1130
+
1131
+	return version;
1132
+}
1133
+
1134
+/* Take a string and replace some variables with something more logical to understand */
1135
+function formatPath(path)
1136
+{
1137
+	const Replacements = [
1138
+		{k:'\\\\', v: '/'},
1139
+		{k:'$boarddir', v:'.'},
1140
+		{k:'$sourcedir', v:'./Sources'},
1141
+		{k:'$avatardir', v:'./avatars'},
1142
+		{k:'$avatars_dir', v:'./avatars'},
1143
+		{k:'$themedir', v:'./Themes/default'},
1144
+		{k:'$imagesdir', v:'./Themes/default/images'},
1145
+		{k:'$themes_dir', v:'./Themes'},
1146
+		{k:'$languagedir', v:'./Themes/default/languages'},
1147
+		{k:'$languages_dir', v:'./Themes/default/languages'},
1148
+		{k:'$smileysdir', v:'./Smileys'},
1149
+		{k:'$smileys_dir', v:'./Smileys'},
1150
+	];
1151
+
1152
+	return Replacements.reduce((rt,cv,ci) => rt.replace(cv['k'],cv['v']), path);
1153
+}
1154
+
1155
+/* The simple JS BBC parser needs some help. */
1156
+async function setBBCodes()
1157
+{
1158
+	bbcParser.add('\\[size=large\\]', '<span style="font-size: 120%;">');
1159
+	bbcParser.add('\\[\\/size\\]', '</span>');
1160
+	bbcParser.add('\n', '<br>');
1161
+	bbcParser.add('\\[code\\](.+?)\\[\\/code\\]', '<pre>$1</pre>');
1162
+}
1163
+
1164
+/* Encode HTML entities made easy */
1165
+function HtmlEncode(s)
1166
+{
1167
+	let el = document.createElement('div');
1168
+	el.innerText = el.textContent = s;
1169
+	return el.innerHTML;
1170
+}
0 1171
\ No newline at end of file
... ...
@@ -0,0 +1,5 @@
1
+This tool parses SMF Packages to show you the instructions that will be peformed.
2
+
3
+This tool is built entirely in javascript and requires no server side utilities.
4
+
5
+I built this as an expierement to see if I could parse packages entirely in the borwser, which is succesful.  However due to CORS, its not possible to fetch remote files.  You can fetch files using the FileName javascript variable.  They must exist on the same domain, or the remote domain must allows CORS/XHR requests.
0 6
\ No newline at end of file
... ...
@@ -0,0 +1,9 @@
1
+# Security Policy
2
+
3
+## Supported Versions
4
+
5
+The current release is the only supported version.  Please test against the latest release prior to submitting a security report.
6
+
7
+## Reporting a Vulnerability
8
+
9
+Use GitHub's Security reporting tool.
0 10
\ No newline at end of file
... ...
@@ -0,0 +1,191 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+	<meta http-equiv="content-type" content="text/html; charset=UTF-8">
5
+	<title>SMF Package Parser</title>
6
+	<meta name="robots" content="noindex, nofollow">
7
+	<meta name="googlebot" content="noindex, nofollow">
8
+	<meta name="viewport" content="width=device-width, initial-scale=1">
9
+	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
10
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js">
11
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js">
12
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js">
13
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/compare-versions@6.0.0-rc.1/lib/umd/index.min.js">
14
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/js-untar@2.0.0/build/dist/untar.min.js">
15
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm">
16
+	<link rel="modulepreload" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/js-bbcode-parser@4.0.0/src/simple.min.js">
17
+	<link rel="modulepreload" href="./Parser.js">
18
+	<link rel="modulepreload" href="./languages/en-us.js">
19
+
20
+	<style>
21
+		pre {tab-size: 4;}
22
+	</style>
23
+</head>
24
+<body>
25
+	<main class="container">
26
+		<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
27
+			<form id="myform" class="me-md-auto">
28
+				<input id="myfile" name="files[]" multiple="" type="file" class="form-control" />
29
+			</form>
30
+
31
+			<a href="#" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none">
32
+				<span class="fs-4" id="packageName">Package Parser</span>
33
+			</a>
34
+
35
+			<form>
36
+				<select id="smfVersions" class="form-select">
37
+					<optgroup id="preferedVersions" label="Prefered SMF Versions"></optgroup>
38
+					<optgroup id="otherVersions" label="Other SMF Versions"></optgroup>
39
+				</select>
40
+			</form>
41
+		</header>
42
+
43
+		<div id="errorContainer" class="alert alert-danger" role="alert" hidden></div>
44
+
45
+		<div id="instructionsContainer" hidden class="container">
46
+		</div>
47
+
48
+		<div id="templateContainer">
49
+			<div id="templateReadme" class="container" hidden>
50
+				<div class="d-flex flex-row">
51
+					<h2 class="p-2">{TITLE}</h2>
52
+					<div class="p-2"><button type="button" class="btn" aria-label="Close">
53
+						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
54
+							<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
55
+						</svg>
56
+					</button></div>
57
+				</div>
58
+				<div id="collapseReadme" class="accordion-collapse collapse show" data-bs-parent="">
59
+					<div class="accordion-body bg-primary-subtle p-2 border">{CONTENT}</div>
60
+				</div>
61
+				<hr>
62
+			</div>
63
+			
64
+			<div id="ciplboard" hidden>
65
+			</div>
66
+
67
+			<div id="templateOperationsTitle" class="container mt-3" hidden>
68
+				<h3>{TITLE}</h3>
69
+				<div class="alert alert-warning" role="alert" hidden></div>
70
+			</div>
71
+			<div id="templateOperations" class="container mb-3" hidden>
72
+				<div class="alert alert-warning" role="alert" hidden></div>
73
+				<div class="alert alert-info" role="alert" hidden></div>
74
+
75
+				<div class="searchContainer d-flex">
76
+					<h5 class="titleSearch d-inline fs-4"></h5>
77
+					<button class="operationSearchCopy d-inline btn pt-0">
78
+						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
79
+							<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
80
+							<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
81
+						</svg>
82
+					</button>
83
+				</div>
84
+				<div class="operationSearch highlight text-bg-light p-3 pb-1 ms-3 border">
85
+					<pre></pre>
86
+				</div>
87
+				<div class="addContainer d-flex">
88
+					<h5 class="titleAdd d-inline fs-4"></h5>
89
+					<button class="operationAddCopy d-inline btn pt-0">
90
+						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
91
+							<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
92
+							<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
93
+						</svg>
94
+					</button>
95
+				</div>
96
+				<div class="operationAdd highlight text-bg-light p-3 pb-1 ms-3 border">
97
+					<pre></pre>
98
+				</div>
99
+			</div>
100
+
101
+			<div id="templateHooks" class="container" hidden>
102
+				<table class="table table-sm table-striped">
103
+					<thead class="table-dark">
104
+						<th>{TITLE}</th>
105
+					</thead>
106
+					<tbody>
107
+					</tbody>
108
+				</table>
109
+				<hr>
110
+			</div>
111
+
112
+			<div id="templateCode" class="container" hidden>
113
+				<table class="table table-sm table-striped">
114
+					<thead class="table-dark">
115
+						<th colspan="2">{TITLE}</th>
116
+					</thead>
117
+					<tbody>
118
+					</tbody>
119
+				</table>
120
+				<hr>
121
+			</div>
122
+
123
+			<div id="templateFileOperations" class="container" hidden>
124
+				<table class="table table-sm table-striped">
125
+					<thead class="table-dark">
126
+						<th>{TITLE}</th>
127
+					</thead>
128
+					<tbody>
129
+					</tbody>
130
+				</table>
131
+				<hr>
132
+			</div>
133
+
134
+			<div id="templateCredits" class="container" hidden>
135
+				<table class="table table-sm table-striped">
136
+					<thead class="table-dark">
137
+						<th>{TITLE}</th>
138
+					</thead>
139
+					<tbody>
140
+					</tbody>
141
+				</table>
142
+				<hr>
143
+			</div>
144
+
145
+		</div>
146
+	</main>
147
+
148
+	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
149
+	<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js" crossorigin="anonymous"></script>
150
+	<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js" crossorigin="anonymous"></script>
151
+	<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/compare-versions@6.0.0-rc.1/lib/umd/index.min.js" crossorigin="anonymous"></script>
152
+	<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-untar@2.0.0/build/dist/untar.min.js" crossorigin="anonymous"></script>
153
+	<script type="module">
154
+		import pako from 'https://cdn.jsdelivr.net/npm/pako@2.1.0/+esm'
155
+		window.pako = pako;
156
+	</script>
157
+	<script type="module">
158
+		import bbCodeParser from 'https://cdn.jsdelivr.net/npm/js-bbcode-parser@4.0.0/src/simple.min.js';
159
+		window.bbcParser = bbCodeParser;
160
+	</script>
161
+
162
+	<script type="text/javascript" src="./Parser.js"></script>
163
+	<script type="text/javascript" src="./languages/en-us.js"></script>
164
+	<script type="text/javascript">
165
+		const SmfVersionApiURL = 'https://custom.simplemachines.org/api.php?action=smf;sa=versions';
166
+		let FileName = '';
167
+
168
+		// Check if we have another language to support.
169
+		const language = window.navigator.language.toLowerCase();
170
+		const supportedLanguages = []; // ['en-us']
171
+		if (supportedLanguages.includes(language))
172
+		{
173
+			const i18n = document.createElement('script');
174
+			i18n.src = './languages/' + language + '.js';
175
+			document.getElementsByTagName('head')[0].appendChild(i18n);
176
+		}
177
+
178
+		// Wait for everything to load, then add a listener on attaching files.
179
+		window.addEventListener('load', async (e) => {
180
+			// Setup the BBC Codes for the parser.
181
+			await setBBCodes();
182
+
183
+			// If we have a file we are defaulting to/passing in
184
+			if (FileName != '')
185
+				runParser(FileName);
186
+
187
+			document.getElementById('myfile').addEventListener('change', uploadFileToJS);
188
+		});
189
+	</script>
190
+</body>
191
+</html>
0 192
\ No newline at end of file
... ...
@@ -0,0 +1,39 @@
1
+txt['title_readme'] = 'Readme';
2
+txt['title_hook'] = 'Integration Hooks';
3
+txt['title_fileop'] = 'File Operations';
4
+txt['title_credits'] = 'Credits';
5
+txt['title_code'] = 'Code';
6
+txt['title_operations'] = 'File Edits';
7
+
8
+// Operations
9
+txt['operation_search'] = 'Find:';
10
+txt['operation_before'] = 'Add Before:';
11
+txt['operation_after'] = 'Add After:';
12
+txt['operation_replace'] = 'Replace With:';
13
+txt['operation_end'] = 'Find (at the end of the file)';
14
+
15
+// Download
16
+txt['download'] = 'Download';
17
+
18
+// Hooks
19
+txt['hook_add'] = 'Add integration function(s) {0} to hook {1}';
20
+txt['hook_pre_include'] = 'Add file(s) {0} to hook integrate_pre_include';
21
+
22
+// File Operations
23
+txt['file_operation_create-dir'] = 'Create a directory "{0}" in "{1}".';
24
+txt['file_operation_create-file'] = 'Create a blank file named "{0}" in "{1}".';
25
+txt['file_operation_require-dir'] = 'Move the included directory "{0}" to "{1}".';
26
+txt['file_operation_require-file'] = 'Move the included file "{0}" to "{1}".';
27
+txt['file_operation_move-dir'] = 'Move the directory "{0}" to "{1}".';
28
+txt['file_operation_move-file'] = 'Move the file "{0}" to "{1}".';
29
+txt['file_operation_remove-dir'] = 'Remove the directory "{0}".';
30
+txt['file_operation_remove-file'] = 'Remove the file "{0}';
31
+
32
+// Credits
33
+txt['credits_version'] = 'Version: ';
34
+txt['credits_license'] = 'License: ';
35
+
36
+// Errors
37
+txt['file_edit_skip_error'] = 'The operations for this file isn\'t vital to the installation of this mod.';
38
+txt['edit_skip_error'] = 'This operation isn\'t vital to the installation of this mod.';
39
+txt['edit_uses_regex'] = 'This operation uses regular expressions and requires a editor capable of searching with regular expressions';
0 40