diff --git a/dsiUnits-js/src/dsiUnit.js b/dsiUnits-js/src/dsiUnit.js index 1a475a66de4d5746d40c42fad9b7838cd8369f55..93de7ae411f4114a9182adf3703ecad617b35212 100644 --- a/dsiUnits-js/src/dsiUnit.js +++ b/dsiUnits-js/src/dsiUnit.js @@ -6,24 +6,23 @@ export class DSIUnit { const parser = DSIParser.getInstance(); const result = parser.parse(dsiString); this.dsiString = result.original; - this.tree = result.tree; // tree: array of fractions (each fraction is an array of nodes) + this.tree = result.tree; this.warnings = result.warnings; this.nonDsiUnit = result.nonDsiUnit; - this.valid = (this.warnings.length === 0); - this.scaleFactor = 1.0; // Stub for now. + // For non-DSI units (or empty string), treat them as "valid" for rendering purposes. + this.valid = this.nonDsiUnit ? true : (this.warnings.length === 0); + this.scaleFactor = 1.0; } toHTML({ wrapper = '', prefix = '', suffix = '' } = {}) { + let htmlOutput = ''; if (this.nonDsiUnit) { + // For non-DSI units, ignore warnings and do not apply error wrapping. 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) { + htmlOutput = content; + } else if (this.tree.length === 1) { htmlOutput = this.tree[0].map(node => node.toHTML()).join('<span class="dsi-mul"> </span>'); } else if (this.tree.length === 2) { - // 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>`; @@ -34,10 +33,18 @@ export class DSIUnit { if (this.scaleFactor !== 1.0) { htmlOutput = `<span class="dsi-scale">${this.scaleFactor}·</span>` + htmlOutput; } - if (!this.valid) { + // Only wrap in error styling if it's a DSI unit (not nonDSI) and not valid. + if (!this.nonDsiUnit && !this.valid) { htmlOutput = `<span class="dsi-error">${htmlOutput}</span>`; } - return `${wrapper}${prefix}${htmlOutput}${suffix}${wrapper}`; + // Add a tooltip if there are warnings (only for DSI units). + let tooltip = ""; + if (!this.nonDsiUnit && this.warnings && this.warnings.length > 0) { + // Replace double backslashes with a single one for display in the tooltip. + const tooltipText = this.warnings.map(w => w.replace(/\\\\/g, '\\')).join("; "); + tooltip = ` title="${tooltipText}"`; + } + return `<span class="dsi-unit-wrapper"${tooltip}>${wrapper}${prefix}${htmlOutput}${suffix}${wrapper}</span>`; } toString() { diff --git a/dsiUnits-js/src/dsiUnitNode.js b/dsiUnits-js/src/dsiUnitNode.js index 59d99c40c9d1a6168101569487f9e6a28559cbba..1b99a8342dee72a0ac1ce6e0cec4089abb115295 100644 --- a/dsiUnits-js/src/dsiUnitNode.js +++ b/dsiUnits-js/src/dsiUnitNode.js @@ -6,7 +6,6 @@ export class DSIUnitNode { this.prefix = prefix; this.unit = unit; this.valid = valid; - // Normalize exponent: if empty or "1", treat as 1. if (exponent === '' || exponent === '1') { this.exponent = 1; } else { @@ -17,7 +16,7 @@ export class DSIUnitNode { } toHTML() { - // Build prefix HTML (if exists) + // Build prefix HTML let prefixHTML = ''; if (this.prefix) { if (this.prefix in dsiPrefixesHTML) { @@ -35,27 +34,24 @@ export class DSIUnitNode { unitHTML = `<span class="dsi-unit"><span class="dsi-invalid">${this.unit}</span></span>`; } } - // Combine the parts let baseHTML = prefixHTML + unitHTML; - // Handle exponent output for non-1 exponents. + // Handle exponent output: if exponent is not 1, check for common fractional exponents. if (this.exponent !== 1) { - // 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>`; + return `<span class="dsi-root"><span class="dsi-root-symbol">√</span><span class="dsi-root-content">${baseHTML}</span></span>`; } else if (Math.abs(this.exponent - 0.333333) < 0.0001) { // Cube root - return `<span class="dsi-sqrt">∛(${baseHTML})</span>`; + return `<span class="dsi-root"><span class="dsi-root-symbol">∛</span><span class="dsi-root-content">${baseHTML}</span></span>`; } else if (Math.abs(this.exponent - 0.25) < 0.0001) { // Fourth root - return `<span class="dsi-sqrt">∜(${baseHTML})</span>`; + return `<span class="dsi-root"><span class="dsi-root-symbol">∜</span><span class="dsi-root-content">${baseHTML}</span></span>`; } else { return `${baseHTML}<sup class="dsi-exponent">${this.exponent}</sup>`; } } else { - // If exponent is non-numeric, simply render as red text return `${baseHTML}<sup class="dsi-exponent"><span class="dsi-invalid">${this.exponent}</span></sup>`; } } diff --git a/dsiUnits-js/tests/dsiUnits.test.js b/dsiUnits-js/tests/dsiUnits.test.js index a20b918a682f10d71c961867ab3b23d8a11db74f..e5069d3e2d65023f4e8f8bbd5beac4d847c481cb 100644 --- a/dsiUnits-js/tests/dsiUnits.test.js +++ b/dsiUnits-js/tests/dsiUnits.test.js @@ -1,37 +1,35 @@ // 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'); 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); - expect(unit.toHTML()).toBe('<span class="dsi-unit">m</span>'); + expect(unit.tree[0][0].unit).toBe('metre'); + expect(unit.tree[0][0].exponent).toBe(1); + expect(unit.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-unit">m</span></span>'); expect(unit.valid).toBe(true); expect(unit.warnings).toEqual([]); }); test('Exponent handling: metre^2 and square-root', () => { const unit2 = new DSIUnit('\\metre\\tothe{2}'); - expect(unit2.toHTML()).toBe('<span class="dsi-unit">m</span><sup class="dsi-exponent">2</sup>'); + expect(unit2.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-unit">m</span><sup class="dsi-exponent">2</sup></span>'); const unitSqrt = new DSIUnit('\\metre\\tothe{0.5}'); - expect(unitSqrt.toHTML()).toBe('<span class="dsi-sqrt">√(<span class="dsi-unit">m</span>)</span>'); + expect(unitSqrt.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-root"><span class="dsi-root-symbol">√</span><span class="dsi-root-content"><span class="dsi-unit">m</span></span></span></span>'); }); test('Fraction: e.g. mega metre per second^2', () => { const unitFrac = new DSIUnit('\\mega\\metre\\per\\second\\tothe{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-unit-wrapper"><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></span>'; expect(unitFrac.toHTML()).toBe(expectedHTML); }); test('Robustness: unknown unit', () => { const unitUnknown = new DSIUnit('\\foo'); - const expectedHTML = '<span class="dsi-error"><span class="dsi-unit"><span class="dsi-invalid">foo</span></span></span>'; + const expectedHTML = '<span class="dsi-unit-wrapper" title="The identifier «foo» does not match any D-SI units!"><span class="dsi-error"><span class="dsi-unit"><span class="dsi-invalid">foo</span></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!'); @@ -39,21 +37,27 @@ 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-unit-wrapper"><span class="dsi-nonunit">NonDsiUnit</span></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-unit-wrapper"><span class="dsi-nonunit">NULL</span></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'); - 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>'; + const expectedHTML = '<span class="dsi-unit-wrapper" title="Double backslash found in string, treating as one backslash: «\\metre\\per\\second»"><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></span>'; expect(unitDouble.toHTML()).toBe(expectedHTML); expect(unitDouble.warnings.some(w => w.includes('Double backslash'))).toBe(true); }); + + test('Fourth root: micro metre tothe{0.25}', () => { + const unitFourth = new DSIUnit('\\micro\\metre\\tothe{0.25}'); + const expectedHTML = '<span class="dsi-unit-wrapper"><span class="dsi-root"><span class="dsi-root-symbol">∜</span><span class="dsi-root-content"><span class="dsi-unit">µ</span><span class="dsi-unit">m</span></span></span></span>'; + expect(unitFourth.toHTML()).toBe(expectedHTML); + }); }); diff --git a/dsiUnits-js/tests/visual_test.html b/dsiUnits-js/tests/visual_test.html index 09b3a0698293af35089c3b96718576e4ea05f989..25f35f8351e211d1568487a9d24edddcd7d68eb8 100644 --- a/dsiUnits-js/tests/visual_test.html +++ b/dsiUnits-js/tests/visual_test.html @@ -2,11 +2,15 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <title>D-SI Units Visual Test</title> + <title>D-SI Units Visual Test & Documentation</title> <style> body { font-family: sans-serif; margin: 20px; + line-height: 1.6; + } + h1, h2, h3 { + color: #333; } table { border-collapse: collapse; @@ -19,27 +23,29 @@ text-align: left; vertical-align: top; } + th { + background-color: #eee; + } pre { margin: 0; font-family: monospace; background: #f8f8f8; padding: 4px; } - /* Invalid parts appear in red */ + /* Invalid tokens in red */ .dsi-invalid { color: red; font-weight: bold; } - /* Overall error: wrap entire output in orange */ + /* Error wrapper (for overall errors) in orange */ .dsi-error { color: orange; } - /* Fraction styling: display as block elements to mimic LaTeX fraction */ + /* Fraction styling */ .dsi-fraction { display: inline-block; text-align: center; vertical-align: middle; - margin: 0 2px; } .dsi-numerator { display: block; @@ -52,21 +58,120 @@ padding: 0 2px; line-height: 1.2; } - /* Sqrt styling */ - .dsi-sqrt { + /* Radical styling */ + .dsi-root { + display: inline-flex; + align-items: flex-start; + } + .dsi-root-symbol { + font-size: 1.2em; + padding-right: 2px; + } + .dsi-root-content { + border-top: 1px solid #000; display: inline-block; - font-family: 'Times New Roman', serif; + padding-top: 2px; } - /* Multiply symbol between units */ .dsi-mul { padding: 0 2px; } + .dsi-unit-wrapper { + /* Wrapper for the final output; tooltip is handled via the title attribute */ + } + /* Documentation explanation styling */ + #documentation { + margin-bottom: 40px; + padding: 10px; + border: 1px solid #ccc; + background-color: #fafafa; + } + /* Styling for the allowed prefixes and units tables */ + .doc-table { + margin-bottom: 20px; + } + .doc-table th { + background-color: #ddd; + } + /* Styling for the interactive section */ + #interactiveSection { + border: 1px solid #ccc; + padding: 10px; + margin-top: 40px; + } + #dsiInput { + width: 400px; + font-family: monospace; + } + #interactiveResult { + margin-top: 10px; + padding: 10px; + border: 1px solid #ccc; + min-height: 40px; + } + #warningText { + color: red; + margin-top: 5px; + } + #liveUpdateContainer { + margin-top: 10px; + } </style> </head> <body> -<h1>D-SI Units Visual Test</h1> +<h1>D-SI Units Visual Test & Documentation</h1> + +<div id="documentation"> + <h2>How D-SI Units Are Composed</h2> + <p> + A D-SI unit string is composed of one or more components, each preceded by a backslash (<code>\</code>). + The components include: + </p> + <ul> + <li>An <strong>optional prefix</strong> (e.g. <code>\kilo</code> for 10³, <code>\milli</code> for 10⁻³),</li> + <li>A <strong>base unit</strong> (e.g. <code>\metre</code>, <code>\second</code>),</li> + <li>An optional exponent specified with <code>\tothe{...}</code> (e.g. <code>\metre\tothe{2}</code> for square metres). Common fractional exponents such as 0.5, 0.333333, and 0.25 are rendered as square, cube, and fourth roots respectively.</li> + <li>An optional <strong>fraction operator</strong> <code>\per</code> to separate numerator and denominator (e.g. <code>\metre\per\second</code> for metres per second).</li> + </ul> + <p> + The rendered output mimics LaTeX-like formatting with radicals (a horizontal line over the radicand) and fractions. + </p> +</div> +<!-- Interactive Section --> +<h2>Interactive D-SI Unit Renderer</h2> +<div id="interactiveSection"> + <label for="dsiInput">Enter D-SI string:</label> + <input type="text" id="dsiInput" placeholder="e.g. \metre\tothe{0.333333}"> + <button id="renderBtn">Render</button> + <div id="liveUpdateContainer"> + <label for="liveUpdate">Live Update: </label> + <input type="checkbox" id="liveUpdate" checked> + </div> + <div id="interactiveResult"></div> +</div> +<h2>Allowed Prefixes</h2> +<table id="prefixes-table" class="doc-table"> + <thead> + <tr> + <th>Prefix Name</th> + <th>Symbol</th> + </tr> + </thead> + <tbody></tbody> +</table> + +<h2>Allowed Units</h2> +<table id="units-table" class="doc-table"> + <thead> + <tr> + <th>Unit Name</th> + <th>Symbol</th> + </tr> + </thead> + <tbody></tbody> +</table> -<h2>Valid D-SI Strings</h2> +<h2>Example D-SI Strings</h2> +<h3>Valid D-SI Strings</h3> <table id="valid-table"> <thead> <tr> @@ -77,7 +182,7 @@ <tbody></tbody> </table> -<h2>Invalid D-SI Strings</h2> +<h3>Invalid D-SI Strings</h3> <table id="invalid-table"> <thead> <tr> @@ -90,9 +195,37 @@ </table> <script type="module"> + // Import the DSIUnit class and unit mappings. import { DSIUnit } from "../src/dsiUnit.js"; + import { dsiPrefixesHTML, dsiUnitsHTML } from "../src/unitStrings.js"; - // Define arrays of sample D-SI strings. + // Populate Allowed Prefixes table. + const prefixesTbody = document.getElementById('prefixes-table').querySelector('tbody'); + Object.keys(dsiPrefixesHTML).sort().forEach(prefix => { + const tr = document.createElement('tr'); + const tdName = document.createElement('td'); + tdName.textContent = prefix; + const tdSymbol = document.createElement('td'); + tdSymbol.textContent = dsiPrefixesHTML[prefix]; + tr.appendChild(tdName); + tr.appendChild(tdSymbol); + prefixesTbody.appendChild(tr); + }); + + // Populate Allowed Units table. + const unitsTbody = document.getElementById('units-table').querySelector('tbody'); + Object.keys(dsiUnitsHTML).sort().forEach(unit => { + const tr = document.createElement('tr'); + const tdName = document.createElement('td'); + tdName.textContent = unit; + const tdSymbol = document.createElement('td'); + tdSymbol.textContent = dsiUnitsHTML[unit]; + tr.appendChild(tdName); + tr.appendChild(tdSymbol); + unitsTbody.appendChild(tr); + }); + + // Sample D-SI strings for the examples. const validDSI = [ '\\metre', '\\metre\\tothe{2}', @@ -100,15 +233,15 @@ '\\mega\\metre\\per\\second\\tothe{2}', '\\kilo\\metre\\per\\hour', '\\metre\\per\\second', - '\\metre\\tothe{0.333333}', // should render as cube root - '\\metre\\tothe{0.25}' // should render as fourth root + '\\metre\\tothe{0.333333}', // Cube root + '\\metre\\tothe{0.25}' // Fourth root ]; const invalidDSI = [ - '\\foo', - '\\milli\\tothe{2}', - '\\metre\\per', - '\\\\metre\\per\\second' + '\\foo', // Unknown unit. + '\\milli\\tothe{2}', // Missing base unit after prefix. + '\\metre\\per', // Fraction missing numerator or denominator. + '\\\\metre\\per\\second' // Double backslashes. ]; function renderDSI(rawStr) { @@ -131,27 +264,53 @@ dsiStrings.forEach(rawStr => { const result = renderDSI(rawStr); const tr = document.createElement('tr'); - const tdRaw = document.createElement('td'); tdRaw.innerHTML = `<pre>${rawStr}</pre>`; tr.appendChild(tdRaw); - const tdRendered = document.createElement('td'); tdRendered.innerHTML = result.html; tr.appendChild(tdRendered); - if (tableId === 'invalid-table') { const tdWarnings = document.createElement('td'); tdWarnings.innerHTML = result.warnings.join('<br>'); tr.appendChild(tdWarnings); } - tbody.appendChild(tr); }); } + // Populate the example tables. populateTable('valid-table', validDSI); populateTable('invalid-table', invalidDSI); + + // Interactive section: re-render on button click or live update if enabled. + const renderBtn = document.getElementById('renderBtn'); + const dsiInput = document.getElementById('dsiInput'); + const liveUpdateCheckbox = document.getElementById('liveUpdate'); + const interactiveResult = document.getElementById('interactiveResult'); + + function updateInteractiveResult() { + const inputStr = dsiInput.value; + try { + const unit = new DSIUnit(inputStr); + const renderedHTML = unit.toHTML(); + let warningsHTML = ""; + if (unit.warnings.length > 0) { + warningsHTML = `<div id="warningText">Warnings: ${unit.warnings.join("; ")}</div>`; + } + interactiveResult.innerHTML = renderedHTML + warningsHTML; + } catch (err) { + interactiveResult.innerHTML = `<span style="color:red;">Error: ${err.message}</span>`; + } + } + + renderBtn.addEventListener('click', updateInteractiveResult); + + dsiInput.addEventListener('input', () => { + if (liveUpdateCheckbox.checked) { + updateInteractiveResult(); + } + }); </script> </body> </html>