From 951dba4cde5cfc6cec08020fff7a119f2a442939 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 4 Sep 2025 10:23:51 +0800 Subject: [PATCH 1/2] Fixed json syntax highlight code and added tests --- .github/workflows/build-and-test.yaml | 17 +++ webroot/js/component/output.js | 8 +- webroot/js/package.json | 3 + webroot/js/test/README.md | 13 +++ webroot/js/test/output.test.mjs | 160 ++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 webroot/js/package.json create mode 100644 webroot/js/test/README.md create mode 100644 webroot/js/test/output.test.mjs diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index c64044a0..ff9ea482 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -7,3 +7,20 @@ jobs: with: java_version: 21 secrets: inherit + + javascript-tests: + name: JavaScript Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Run JavaScript tests + run: | + cd webroot/js/test + node --test *.test.mjs diff --git a/webroot/js/component/output.js b/webroot/js/component/output.js index c7aef5e2..22cb242f 100644 --- a/webroot/js/component/output.js +++ b/webroot/js/component/output.js @@ -17,10 +17,10 @@ function highlightJSON(json) { return json .replace(/("[\w\s_-]+")(\s*:)/g, '$1$2') - .replace(/:\s*(".*?")/g, ': $1') - .replace(/:\s*(\d+\.?\d*)/g, ': $1') - .replace(/:\s*(true|false)/g, ': $1') - .replace(/:\s*(null)/g, ': $1'); + .replace(/(:\s*)("(?:[^"\\]|\\.)*")/g, '$1$2') + .replace(/(:\s*|[\[\,]\s*)(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)(?=\s*[,\]\}\n]|$)/g, '$1$2') + .replace(/(:\s*|[\[\,]\s*)(true|false)(?=\s*[,\]\}\n]|$)/g, '$1$2') + .replace(/(:\s*|[\[\,]\s*)(null)(?=\s*[,\]\}\n]|$)/g, '$1$2'); } function formatOutput(data) { diff --git a/webroot/js/package.json b/webroot/js/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/webroot/js/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/webroot/js/test/README.md b/webroot/js/test/README.md new file mode 100644 index 00000000..495deae9 --- /dev/null +++ b/webroot/js/test/README.md @@ -0,0 +1,13 @@ +# JavaScript Unit Tests + +This directory contains Node.js unit tests using ES modules and Node.js's built-in `node:test` module. + +To run the tests, you need Node.js v18+ installed. Then run: + +```bash +# Run one test file +node --test webroot/js/test/output.test.mjs + +# Run all tests in the directory +node --test webroot/js/test/ +``` diff --git a/webroot/js/test/output.test.mjs b/webroot/js/test/output.test.mjs new file mode 100644 index 00000000..a463482c --- /dev/null +++ b/webroot/js/test/output.test.mjs @@ -0,0 +1,160 @@ +import { test, describe } from 'node:test'; +import assert from 'node:assert'; +import { highlightJSON } from '../component/output.js'; + +describe('highlightJSON', () => { + test('should highlight numbers', () => { + const input = { count: 42 }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"count":42}'; + assert.strictEqual(result, expected); + }); + + test('should not highlight numbers within strings', () => { + const input = { + link_id: "aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec" + }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"link_id":"aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec"}'; + assert.strictEqual(result, expected); + }); + + test('should handle mixed numbers and strings', () => { + const input = { + service_id: 8, + site_id: 276, + link_id: "aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec" + }; + const result = highlightJSON(JSON.stringify(input, null, 2)); + + const expected = `{ + "service_id": 8, + "site_id": 276, + "link_id": "aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec" +}`; + assert.strictEqual(result, expected); + }); + + test('should highlight strings', () => { + const input = { name: "test-value" }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"name":"test-value"}'; + assert.strictEqual(result, expected); + }); + + test('should highlight keys', () => { + const input = { test_key: "value" }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"test_key":"value"}'; + assert.strictEqual(result, expected); + }); + + test('should highlight booleans', () => { + const input = { flag: true, disabled: false }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"flag":true,"disabled":false}'; + assert.strictEqual(result, expected); + }); + + test('should highlight null', () => { + const input = { value: null }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"value":null}'; + assert.strictEqual(result, expected); + }); + + test('should handle negative numbers', () => { + const input = { temperature: -15 }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"temperature":-15}'; + assert.strictEqual(result, expected); + }); + + test('should handle decimal numbers', () => { + const input = { price: 19.99 }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"price":19.99}'; + assert.strictEqual(result, expected); + }); + + test('should handle scientific notation', () => { + const input = { value: 1.23e-4 }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"value":0.000123}'; + assert.strictEqual(result, expected); + }); + + test('should handle nested objects', () => { + const input = { + user: { + id: 123, + name: "John Doe", + active: true, + metadata: null, + scores: [95.5, -2.3, 0] + } + }; + const result = highlightJSON(JSON.stringify(input, null, 2)); + + const expected = `{ + "user": { + "id": 123, + "name": "John Doe", + "active": true, + "metadata": null, + "scores": [ + 95.5, + -2.3, + 0 + ] + } +}`; + assert.strictEqual(result, expected); + }); + + test('should handle escaped quotes', () => { + const input = { + message: 'He said "Hello, world!" to everyone' + }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"message":"He said \\"Hello, world!\\" to everyone"}'; + assert.strictEqual(result, expected); + }); + + test('should handle empty objects and arrays', () => { + const input = { empty_obj: {}, empty_array: [] }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"empty_obj":{},"empty_array":[]}'; + assert.strictEqual(result, expected); + }); + + test('should handle special characters in strings', () => { + const input = { + special: "Line 1\nLine 2\tTabbed\r\nWindows line ending", + unicode: "Unicode: 🚀 ñ é" + }; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '{"special":"Line 1\\nLine 2\\tTabbed\\r\\nWindows line ending","unicode":"Unicode: 🚀 ñ é"}'; + assert.strictEqual(result, expected); + }); + + test('should handle non-object input', () => { + const input = [1, 2, 3]; + const result = highlightJSON(JSON.stringify(input)); + + const expected = '[1,2,3]'; + assert.strictEqual(result, expected); + }); +}); From fab013b8f6402c05c6041c4dbc870f7500f2e603 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 4 Sep 2025 11:06:24 +0800 Subject: [PATCH 2/2] More readable regex --- webroot/js/component/output.js | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/webroot/js/component/output.js b/webroot/js/component/output.js index 22cb242f..b5c5b257 100644 --- a/webroot/js/component/output.js +++ b/webroot/js/component/output.js @@ -15,12 +15,45 @@ function highlightJSON(json) { json = JSON.stringify(json, null, 2); } + // Common regex patterns for JSON syntax highlighting + + // Matches optional whitespace (spaces, tabs, newlines) + // Examples: "", " ", "\t", "\n", " \n " + const optionalWhitespace = '\\s*'; + + // Matches JSON characters that precede a value: colon followed by optional whitespace, or array/object start with optional whitespace + // Examples: ": ", ":[", ", ", "[ ", "[42" + const valuePrefixes = '(:\\s*|[\\[\\,]\\s*)'; + + // Matches JSON characters that follow a value: comma, closing brackets/braces, newline, or end of string + // Examples: ",", "]", "}", "\n", end of string + const valueEndings = '(?=\\s*[,\\]\\}\\n]|$)'; + + // Matches quoted strings with escaped characters + // Examples: "hello", "world \"quoted\"", "path\\to\\file", "unicode: \\u0041" + const quotedString = '"(?:[^"\\\\]|\\\\.)*"'; + + // Matches JSON object keys (quoted strings containing word chars, spaces, underscores, hyphens) + // Examples: "name", "user_id", "first-name", "my key", "data_2023" + const objectKey = '"[\\w\\s_-]+"'; + + // Matches numbers (integer, decimal, scientific notation) + // Examples: 42, -17, 3.14, -0.5, 1e10, 2.5e-3, 1E+5 + const number = '-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?'; + + // Complete regex patterns for each replacement + const keyPattern = new RegExp(`(${objectKey})(${optionalWhitespace}:)`, 'g'); // "name": or "user_id" : + const stringValuePattern = new RegExp(`(:\\s*)(${quotedString})`, 'g'); // : "John Doe" or :"hello" + const numberPattern = new RegExp(`${valuePrefixes}(${number})${valueEndings}`, 'g'); // : 42, [123, or , -3.14 + const booleanPattern = new RegExp(`${valuePrefixes}(true|false)${valueEndings}`, 'g'); // : true, [false, or , true + const nullPattern = new RegExp(`${valuePrefixes}(null)${valueEndings}`, 'g'); // : null, [null, or , null + return json - .replace(/("[\w\s_-]+")(\s*:)/g, '$1$2') - .replace(/(:\s*)("(?:[^"\\]|\\.)*")/g, '$1$2') - .replace(/(:\s*|[\[\,]\s*)(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)(?=\s*[,\]\}\n]|$)/g, '$1$2') - .replace(/(:\s*|[\[\,]\s*)(true|false)(?=\s*[,\]\}\n]|$)/g, '$1$2') - .replace(/(:\s*|[\[\,]\s*)(null)(?=\s*[,\]\}\n]|$)/g, '$1$2'); + .replace(keyPattern, '$1$2') + .replace(stringValuePattern, '$1$2') + .replace(numberPattern, '$1$2') + .replace(booleanPattern, '$1$2') + .replace(nullPattern, '$1$2'); } function formatOutput(data) {