Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 38 additions & 5 deletions webroot/js/component/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<span class="json-key">$1</span>$2')
.replace(/:\s*(".*?")/g, ': <span class="json-string">$1</span>')
.replace(/:\s*(\d+\.?\d*)/g, ': <span class="json-number">$1</span>')
.replace(/:\s*(true|false)/g, ': <span class="json-boolean">$1</span>')
.replace(/:\s*(null)/g, ': <span class="json-null">$1</span>');
.replace(keyPattern, '<span class="json-key">$1</span>$2')
.replace(stringValuePattern, '$1<span class="json-string">$2</span>')
.replace(numberPattern, '$1<span class="json-number">$2</span>')
.replace(booleanPattern, '$1<span class="json-boolean">$2</span>')
.replace(nullPattern, '$1<span class="json-null">$2</span>');
}

function formatOutput(data) {
Expand Down
3 changes: 3 additions & 0 deletions webroot/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
13 changes: 13 additions & 0 deletions webroot/js/test/README.md
Original file line number Diff line number Diff line change
@@ -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/
```
160 changes: 160 additions & 0 deletions webroot/js/test/output.test.mjs
Original file line number Diff line number Diff line change
@@ -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 = '{<span class="json-key">"count"</span>:<span class="json-number">42</span>}';
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 = '{<span class="json-key">"link_id"</span>:<span class="json-string">"aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec"</span>}';
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 = `{
<span class="json-key">"service_id"</span>: <span class="json-number">8</span>,
<span class="json-key">"site_id"</span>: <span class="json-number">276</span>,
<span class="json-key">"link_id"</span>: <span class="json-string">"aws:us-west-2:94889860-0a55-4fbd-981e-3a9ce99ca1ec"</span>
}`;
assert.strictEqual(result, expected);
});

test('should highlight strings', () => {
const input = { name: "test-value" };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"name"</span>:<span class="json-string">"test-value"</span>}';
assert.strictEqual(result, expected);
});

test('should highlight keys', () => {
const input = { test_key: "value" };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"test_key"</span>:<span class="json-string">"value"</span>}';
assert.strictEqual(result, expected);
});

test('should highlight booleans', () => {
const input = { flag: true, disabled: false };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"flag"</span>:<span class="json-boolean">true</span>,<span class="json-key">"disabled"</span>:<span class="json-boolean">false</span>}';
assert.strictEqual(result, expected);
});

test('should highlight null', () => {
const input = { value: null };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"value"</span>:<span class="json-null">null</span>}';
assert.strictEqual(result, expected);
});

test('should handle negative numbers', () => {
const input = { temperature: -15 };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"temperature"</span>:<span class="json-number">-15</span>}';
assert.strictEqual(result, expected);
});

test('should handle decimal numbers', () => {
const input = { price: 19.99 };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"price"</span>:<span class="json-number">19.99</span>}';
assert.strictEqual(result, expected);
});

test('should handle scientific notation', () => {
const input = { value: 1.23e-4 };
const result = highlightJSON(JSON.stringify(input));

const expected = '{<span class="json-key">"value"</span>:<span class="json-number">0.000123</span>}';
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 = `{
<span class="json-key">"user"</span>: {
<span class="json-key">"id"</span>: <span class="json-number">123</span>,
<span class="json-key">"name"</span>: <span class="json-string">"John Doe"</span>,
<span class="json-key">"active"</span>: <span class="json-boolean">true</span>,
<span class="json-key">"metadata"</span>: <span class="json-null">null</span>,
<span class="json-key">"scores"</span>: [
<span class="json-number">95.5</span>,
<span class="json-number">-2.3</span>,
<span class="json-number">0</span>
]
}
}`;
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 = '{<span class="json-key">"message"</span>:<span class="json-string">"He said \\"Hello, world!\\" to everyone"</span>}';
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 = '{<span class="json-key">"empty_obj"</span>:{},<span class="json-key">"empty_array"</span>:[]}';
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 = '{<span class="json-key">"special"</span>:<span class="json-string">"Line 1\\nLine 2\\tTabbed\\r\\nWindows line ending"</span>,<span class="json-key">"unicode"</span>:<span class="json-string">"Unicode: 🚀 ñ é"</span>}';
assert.strictEqual(result, expected);
});

test('should handle non-object input', () => {
const input = [1, 2, 3];
const result = highlightJSON(JSON.stringify(input));

const expected = '[<span class="json-number">1</span>,<span class="json-number">2</span>,<span class="json-number">3</span>]';
assert.strictEqual(result, expected);
});
});