diff --git a/dsiUnits-js/src/dsiUnit.js b/dsiUnits-js/src/dsiUnit.js index 4a258f9472a1ce19758cca616ca2c769cde1b9b8..1a475a66de4d5746d40c42fad9b7838cd8369f55 100644 --- a/dsiUnits-js/src/dsiUnit.js +++ b/dsiUnits-js/src/dsiUnit.js @@ -1,68 +1,46 @@ -// dsiUnit.js +// src/dsiUnit.js import DSIParser from './dsiParser.js'; -import { DSIUnitNode } from './dsiUnitNode.js'; export class DSIUnit { constructor(dsiString) { - // Use the parser to parse the string. const parser = DSIParser.getInstance(); const result = parser.parse(dsiString); this.dsiString = result.original; - this.tree = result.tree; // tree is an array of fractions (each fraction is an array of DSIUnitNode) + this.tree = result.tree; // tree: array of fractions (each fraction is an array of nodes) this.warnings = result.warnings; this.nonDsiUnit = result.nonDsiUnit; this.valid = (this.warnings.length === 0); - // Stub: scaleFactor will remain 1.0 - this.scaleFactor = 1.0; + this.scaleFactor = 1.0; // Stub for now. } - /** - * Generates HTML output representing the D-SI unit. - * For fractions, output a fraction structure. - */ toHTML({ wrapper = '', prefix = '', suffix = '' } = {}) { - // If non-DSI unit, simply wrap it in a special span. if (this.nonDsiUnit) { const content = `<span class="dsi-nonunit">${this.dsiString.startsWith('|') ? this.dsiString.slice(1) : this.dsiString}</span>`; return `${wrapper}${prefix}${content}${suffix}${wrapper}`; } let htmlOutput = ''; - if (this.tree.length === 1) { - // No fraction: simply join the nodes with a small separator. htmlOutput = this.tree[0].map(node => node.toHTML()).join('<span class="dsi-mul"> </span>'); } else if (this.tree.length === 2) { - // Fraction: generate numerator and denominator. + // Render as a fraction in one line (no extra whitespace) const numerator = this.tree[0].map(node => node.toHTML()).join('<span class="dsi-mul"> </span>'); const denominator = this.tree[1].map(node => node.toHTML()).join('<span class="dsi-mul"> </span>'); htmlOutput = `<span class="dsi-fraction"><span class="dsi-numerator">${numerator}</span><span class="dsi-denom">${denominator}</span></span>`; } else { - // More than two fractions, join with a red slash as a warning. - htmlOutput = this.tree.map((frac, idx) => { - const part = frac.map(node => node.toHTML()).join('<span class="dsi-mul"> </span>'); - return part; - }).join('<span class="dsi-invalid">/</span>'); + htmlOutput = this.tree.map(frac => frac.map(node => node.toHTML()).join('<span class="dsi-mul"> </span>')) + .join('<span class="dsi-invalid">/</span>'); } - // If scaleFactor is not 1.0, prepend it. if (this.scaleFactor !== 1.0) { htmlOutput = `<span class="dsi-scale">${this.scaleFactor}·</span>` + htmlOutput; } + if (!this.valid) { + htmlOutput = `<span class="dsi-error">${htmlOutput}</span>`; + } return `${wrapper}${prefix}${htmlOutput}${suffix}${wrapper}`; } - // Stub methods for math operations: - getScaleFactor(other) { - // Stub: not implemented - return NaN; - } - getBaseUnit(other) { - // Stub: not implemented - return null; - } - toString() { - // A simple text representation that joins all nodes if (this.nonDsiUnit) return '|' + this.dsiString; return this.tree.map(frac => frac.map(node => node.toString()).join('')).join('\\per'); } diff --git a/dsiUnits-js/src/dsiUnitNode.js b/dsiUnits-js/src/dsiUnitNode.js index d5fe1691fce340a47dd3fe43b7fd48f4675f9ba1..59d99c40c9d1a6168101569487f9e6a28559cbba 100644 --- a/dsiUnits-js/src/dsiUnitNode.js +++ b/dsiUnits-js/src/dsiUnitNode.js @@ -1,4 +1,4 @@ -// dsiUnitNode.js +// src/dsiUnitNode.js import { dsiPrefixesHTML, dsiUnitsHTML } from './unitStrings.js'; export class DSIUnitNode { @@ -17,27 +17,46 @@ export class DSIUnitNode { } toHTML() { - let parts = []; + // Build prefix HTML (if exists) + let prefixHTML = ''; if (this.prefix) { if (this.prefix in dsiPrefixesHTML) { - parts.push(`<span class="dsi-unit">${dsiPrefixesHTML[this.prefix]}</span>`); + prefixHTML = `<span class="dsi-unit">${dsiPrefixesHTML[this.prefix]}</span>`; } else { - parts.push(`<span class="dsi-unit"><span class="dsi-invalid">${this.prefix}</span></span>`); + prefixHTML = `<span class="dsi-unit"><span class="dsi-invalid">${this.prefix}</span></span>`; } } + // Build unit HTML + let unitHTML = ''; if (this.unit) { if (this.unit in dsiUnitsHTML) { - parts.push(`<span class="dsi-unit">${dsiUnitsHTML[this.unit]}</span>`); + unitHTML = `<span class="dsi-unit">${dsiUnitsHTML[this.unit]}</span>`; } else { - parts.push(`<span class="dsi-unit"><span class="dsi-invalid">${this.unit}</span></span>`); + unitHTML = `<span class="dsi-unit"><span class="dsi-invalid">${this.unit}</span></span>`; } } - let baseHTML = parts.join(''); + // Combine the parts + let baseHTML = prefixHTML + unitHTML; + + // Handle exponent output for non-1 exponents. if (this.exponent !== 1) { - if (this.exponent === 0.5) { - return `<span class="dsi-sqrt">√(${baseHTML})</span>`; + // If exponent is numeric, check for common fractional values. + if (typeof this.exponent === 'number') { + if (Math.abs(this.exponent - 0.5) < 0.0001) { + // Square root + return `<span class="dsi-sqrt">√(${baseHTML})</span>`; + } else if (Math.abs(this.exponent - 0.333333) < 0.0001) { + // Cube root + return `<span class="dsi-sqrt">∛(${baseHTML})</span>`; + } else if (Math.abs(this.exponent - 0.25) < 0.0001) { + // Fourth root + return `<span class="dsi-sqrt">∜(${baseHTML})</span>`; + } else { + return `${baseHTML}<sup class="dsi-exponent">${this.exponent}</sup>`; + } } else { - return `${baseHTML}<sup class="dsi-exponent">${this.exponent}</sup>`; + // If exponent is non-numeric, simply render as red text + return `${baseHTML}<sup class="dsi-exponent"><span class="dsi-invalid">${this.exponent}</span></sup>`; } } return baseHTML; diff --git a/dsiUnits-js/tests/dsiUnits.test.js b/dsiUnits-js/tests/dsiUnits.test.js index cddb15bb2bbf474620c5c2c1148fdb47db1d7400..a20b918a682f10d71c961867ab3b23d8a11db74f 100644 --- a/dsiUnits-js/tests/dsiUnits.test.js +++ b/dsiUnits-js/tests/dsiUnits.test.js @@ -1,17 +1,15 @@ -// dsiUnits.test.js +// tests/dsiUnits.test.js import { DSIUnit } from '../src/dsiUnit.js'; import { DSIUnitNode } from '../src/dsiUnitNode.js'; describe('DSIUnit HTML Output Tests', () => { test('Base case: one unit without prefix or exponent', () => { const unit = new DSIUnit('\\metre'); - // We expect a single node with unit "metre" and exponent 1 expect(unit.tree.length).toBe(1); expect(unit.tree[0].length).toBe(1); const node = unit.tree[0][0]; expect(node.unit).toBe('metre'); expect(node.exponent).toBe(1); - // Check HTML output expect(unit.toHTML()).toBe('<span class="dsi-unit">m</span>'); expect(unit.valid).toBe(true); expect(unit.warnings).toEqual([]); @@ -27,14 +25,13 @@ describe('DSIUnit HTML Output Tests', () => { test('Fraction: e.g. mega metre per second^2', () => { const unitFrac = new DSIUnit('\\mega\\metre\\per\\second\\tothe{2}'); - // Expected: numerator: mega metre; denominator: second^2 - const expectedHTML = `<span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">M</span><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span><sup class="dsi-exponent">2</sup></span></span>`; + const expectedHTML = '<span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">M</span><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span><sup class="dsi-exponent">2</sup></span></span>'; expect(unitFrac.toHTML()).toBe(expectedHTML); }); test('Robustness: unknown unit', () => { const unitUnknown = new DSIUnit('\\foo'); - const expectedHTML = `<span class="dsi-unit"><span class="dsi-invalid">foo</span></span>`; + const expectedHTML = '<span class="dsi-error"><span class="dsi-unit"><span class="dsi-invalid">foo</span></span></span>'; expect(unitUnknown.toHTML()).toBe(expectedHTML); expect(unitUnknown.valid).toBe(false); expect(unitUnknown.warnings).toContain('The identifier «foo» does not match any D-SI units!'); @@ -42,21 +39,20 @@ describe('DSIUnit HTML Output Tests', () => { test('Non D-SI unit marker', () => { const nonDsi = new DSIUnit('|NonDsiUnit'); - const expectedHTML = `<span class="dsi-nonunit">NonDsiUnit</span>`; + const expectedHTML = '<span class="dsi-nonunit">NonDsiUnit</span>'; expect(nonDsi.toHTML()).toBe(expectedHTML); }); test('Empty string', () => { const emptyUnit = new DSIUnit(''); - const expectedHTML = `<span class="dsi-nonunit">NULL</span>`; + const expectedHTML = '<span class="dsi-nonunit">NULL</span>'; expect(emptyUnit.toHTML()).toBe(expectedHTML); expect(emptyUnit.warnings).toContain('Given D-SI string is empty!'); }); test('Double backslash handling', () => { const unitDouble = new DSIUnit('\\\\metre\\per\\second'); - // Should be treated as single backslashes. - const expectedHTML = `<span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span></span></span>`; + const expectedHTML = '<span class="dsi-error"><span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span></span></span></span>'; expect(unitDouble.toHTML()).toBe(expectedHTML); expect(unitDouble.warnings.some(w => w.includes('Double backslash'))).toBe(true); }); diff --git a/dsiUnits-js/tests/visual_test.html b/dsiUnits-js/tests/visual_test.html index f2a9a86c0947dde899a4186cd97421d9fc7a752a..09b3a0698293af35089c3b96718576e4ea05f989 100644 --- a/dsiUnits-js/tests/visual_test.html +++ b/dsiUnits-js/tests/visual_test.html @@ -25,12 +25,41 @@ background: #f8f8f8; padding: 4px; } + /* Invalid parts appear in red */ .dsi-invalid { color: red; font-weight: bold; } + /* Overall error: wrap entire output in orange */ + .dsi-error { + color: orange; + } + /* Fraction styling: display as block elements to mimic LaTeX fraction */ + .dsi-fraction { + display: inline-block; + text-align: center; + vertical-align: middle; + margin: 0 2px; + } + .dsi-numerator { + display: block; + border-bottom: 1px solid #000; + padding: 0 2px; + line-height: 1.2; + } + .dsi-denom { + display: block; + padding: 0 2px; + line-height: 1.2; + } + /* Sqrt styling */ .dsi-sqrt { - font-style: italic; + display: inline-block; + font-family: 'Times New Roman', serif; + } + /* Multiply symbol between units */ + .dsi-mul { + padding: 0 2px; } </style> </head> @@ -61,32 +90,27 @@ </table> <script type="module"> - // Import your DSIUnit class from the src directory. import { DSIUnit } from "../src/dsiUnit.js"; // Define arrays of sample D-SI strings. - // Valid examples now include ones using \per as well as fractional exponents tothe{0.333333} and tothe{0.25}. const validDSI = [ '\\metre', '\\metre\\tothe{2}', '\\metre\\tothe{0.5}', '\\mega\\metre\\per\\second\\tothe{2}', '\\kilo\\metre\\per\\hour', - '\\metre\\per\\second', // simple fraction: metre per second - '\\metre\\tothe{0.333333}', // fractional exponent ~ one-third power - '\\metre\\tothe{0.25}' // fractional exponent ~ one-quarter power + '\\metre\\per\\second', + '\\metre\\tothe{0.333333}', // should render as cube root + '\\metre\\tothe{0.25}' // should render as fourth root ]; - // Some invalid examples. const invalidDSI = [ - '\\foo', // Unknown unit. - '\\milli\\tothe{2}', // Missing base unit after prefix. - '\\metre\\per', // Fraction missing numerator or denominator. - '\\\\metre\\per\\second' // Double backslashes. + '\\foo', + '\\milli\\tothe{2}', + '\\metre\\per', + '\\\\metre\\per\\second' ]; - // This function creates a new DSIUnit instance from a raw string, - // calls its toHTML() method, and returns an object with the rendered HTML and warnings. function renderDSI(rawStr) { try { const dsi = new DSIUnit(rawStr); @@ -102,24 +126,20 @@ } } - // Populate the given table (by its ID) with rows for each raw string. function populateTable(tableId, dsiStrings) { const tbody = document.getElementById(tableId).querySelector('tbody'); dsiStrings.forEach(rawStr => { const result = renderDSI(rawStr); const tr = document.createElement('tr'); - // Raw string in teletype const tdRaw = document.createElement('td'); tdRaw.innerHTML = `<pre>${rawStr}</pre>`; tr.appendChild(tdRaw); - // Rendered output const tdRendered = document.createElement('td'); tdRendered.innerHTML = result.html; tr.appendChild(tdRendered); - // If this is the invalid table, add a warnings column. if (tableId === 'invalid-table') { const tdWarnings = document.createElement('td'); tdWarnings.innerHTML = result.warnings.join('<br>'); @@ -130,7 +150,6 @@ }); } - // Populate both tables. populateTable('valid-table', validDSI); populateTable('invalid-table', invalidDSI); </script>