diff --git a/CHANGELOG.md b/CHANGELOG.md index d377570..a01a9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,3 +39,8 @@ - Built-in date/time recognition for ISO 8601 and common local formats - New `parseDates` option to enable the feature + +## 2.3.0 (2025-06-09) + +- URL and file path detection via `parseUrls` and `parseFilePaths` options +- New examples and benchmarks covering the feature diff --git a/README.md b/README.md index 803afb5..06e6b49 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ A small utility that automatically converts strings and other values into the mo - Supports typed arrays - Evaluates simple math expressions - Recognizes common date/time formats +- Detects URLs and file-system paths - Optional environment variable expansion - Optional function-string parsing - Advanced features are disabled by default and can be enabled individually @@ -62,6 +63,8 @@ autoParse('Map:[["a",1]]', { parseMapSets: true }).get('a') // => 1 autoParse('Uint8Array[1,2]', { parseTypedArrays: true })[0] // => 1 autoParse('2 + 3 * 4', { parseExpressions: true }) // => 14 autoParse('2023-06-01', { parseDates: true }) // => Date object +autoParse('http://example.com', { parseUrls: true }) // => URL instance +autoParse('./foo/bar', { parseFilePaths: true }) // => normalized path process.env.TEST_ENV = '123' autoParse('$TEST_ENV', { expandEnv: true }) // => 123 const double = autoParse('x => x * 2', { parseFunctionStrings: true }) @@ -126,9 +129,11 @@ More examples can be found in the [`examples/`](examples) directory. - `parseTypedArrays` – support typed array notation. - `parseExpressions` – evaluate simple math expressions. - `parseDates` – recognize ISO 8601 and common local date/time strings. +- `parseUrls` – detect valid URLs and return `URL` objects. +- `parseFilePaths` – detect file-system paths and normalize them. - `currencySymbols` – object mapping extra currency symbols to codes, e.g. `{ 'r$': 'BRL', "\u20BA": 'TRY' }`. -## Benchmarks (v2.2.0) +## Benchmarks (v2.3.0) The following timings are measured on Node.js using `npm test` and represent roughly how long it takes to parse 10 000 values after warm‑up: @@ -143,6 +148,8 @@ The following timings are measured on Node.js using `npm test` and represent rou | options combined | ~6 | | plugin hook | ~4 | | date/time parse | ~5 | +| URL parse | ~5 | +| file path parse | ~5 | Even a single parse is extremely fast: @@ -157,6 +164,8 @@ Even a single parse is extremely fast: | options combined | ~0.0006 | | plugin hook | ~0.0004 | | date/time parse | ~0.0005 | +| URL parse | ~0.0005 | +| file path parse | ~0.0005 | These numbers demonstrate the parser runs in well under a millisecond for typical values, so performance should never be a concern. @@ -194,6 +203,9 @@ optional environment variable and function-string handling. See Version 2.2 introduces optional date/time recognition. See [docs/RELEASE_NOTES_2.2.md](docs/RELEASE_NOTES_2.2.md) for details. +Version 2.3 adds URL and file path detection. See +[docs/RELEASE_NOTES_2.3.md](docs/RELEASE_NOTES_2.3.md) for details. + ## Contributing 1. Fork the repository and create a branch for your feature or fix. diff --git a/dist/auto-parse.esm.js b/dist/auto-parse.esm.js index a135678..20d529e 100644 --- a/dist/auto-parse.esm.js +++ b/dist/auto-parse.esm.js @@ -252,6 +252,20 @@ var require_auto_parse = __commonJS({ } return null; } + function parseUrlString(str) { + try { + return new URL(str); + } catch (e) { + return null; + } + } + function parseFilePathString(str) { + const re = /^(?:[A-Za-z]:[\\/]|\\\\|\.{1,2}[\\/]|~[\\/]|\/)/; + if (re.test(str)) { + return str.replace(/\\+/g, "/").replace(/\/+/g, "/"); + } + return null; + } function parseExpressionString(str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -338,6 +352,11 @@ var require_auto_parse = __commonJS({ return new Map(autoParse(value, options)); case "set": return new Set(autoParse(value, options)); + case "url": + return new URL(value); + case "path": + case "filepath": + return parseFilePathString(String(value)) || String(value); default: if (typeof type === "function") { if (/Array$/.test(type.name)) { @@ -466,6 +485,16 @@ var require_auto_parse = __commonJS({ if (dt) return returnIfAllowed(dt, options, originalValue); } + if (options.parseUrls) { + const u = parseUrlString(trimmed); + if (u) + return returnIfAllowed(u, options, originalValue); + } + if (options.parseFilePaths) { + const p = parseFilePathString(trimmed); + if (p) + return returnIfAllowed(p, options, originalValue); + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })); if (value === "undefined" || value === "") { return returnIfAllowed(void 0, options, originalValue); diff --git a/dist/auto-parse.js b/dist/auto-parse.js index 1bad518..80e2e7e 100644 --- a/dist/auto-parse.js +++ b/dist/auto-parse.js @@ -245,6 +245,20 @@ function parseDateTimeString(str) { } return null; } +function parseUrlString(str) { + try { + return new URL(str); + } catch (e) { + return null; + } +} +function parseFilePathString(str) { + const re = /^(?:[A-Za-z]:[\\/]|\\\\|\.{1,2}[\\/]|~[\\/]|\/)/; + if (re.test(str)) { + return str.replace(/\\+/g, "/").replace(/\/+/g, "/"); + } + return null; +} function parseExpressionString(str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -331,6 +345,11 @@ function parseType(value, type, options = {}) { return new Map(autoParse(value, options)); case "set": return new Set(autoParse(value, options)); + case "url": + return new URL(value); + case "path": + case "filepath": + return parseFilePathString(String(value)) || String(value); default: if (typeof type === "function") { if (/Array$/.test(type.name)) { @@ -459,6 +478,16 @@ function autoParse(value, typeOrOptions) { if (dt) return returnIfAllowed(dt, options, originalValue); } + if (options.parseUrls) { + const u = parseUrlString(trimmed); + if (u) + return returnIfAllowed(u, options, originalValue); + } + if (options.parseFilePaths) { + const p = parseFilePathString(trimmed); + if (p) + return returnIfAllowed(p, options, originalValue); + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })); if (value === "undefined" || value === "") { return returnIfAllowed(void 0, options, originalValue); diff --git a/docs/RELEASE_NOTES_2.3.md b/docs/RELEASE_NOTES_2.3.md new file mode 100644 index 0000000..c8899f8 --- /dev/null +++ b/docs/RELEASE_NOTES_2.3.md @@ -0,0 +1,9 @@ +# Release Notes: Version 2.3 + +Version 2.3 introduces optional URL and file path parsing. + +- URLs such as `https://example.com` return `URL` objects when `parseUrls` is enabled. +- File-system paths like `./foo/bar` normalize to platform-neutral strings when `parseFilePaths` is enabled. +- Both features are disabled by default and can be turned on individually via options. + +See the [CHANGELOG](../CHANGELOG.md) for the full history. diff --git a/examples/README.md b/examples/README.md index ac0d368..0d52a8b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,4 +7,5 @@ them with `node ` from this directory. - `plugin.js` illustrates registering a simple plugin. - `types.js` covers advanced types like `BigInt` and `Symbol`. - `dates.js` demonstrates the optional date/time parsing capability. +- `urls.js` shows URL and file path detection. - `all-options.js` exercises every available option in one script. diff --git a/examples/urls.js b/examples/urls.js new file mode 100644 index 0000000..3cbba18 --- /dev/null +++ b/examples/urls.js @@ -0,0 +1,4 @@ +const autoParse = require('..') + +console.log('URL:', autoParse('https://example.com', { parseUrls: true })) +console.log('Path:', autoParse('./foo/bar', { parseFilePaths: true })) diff --git a/index.d.ts b/index.d.ts index e1b146c..0b78832 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,6 +18,8 @@ export interface AutoParseOptions { parseTypedArrays?: boolean; parseExpressions?: boolean; parseDates?: boolean; + parseUrls?: boolean; + parseFilePaths?: boolean; type?: any; } export type Parser = (value: any, type?: any, options?: AutoParseOptions) => any | undefined; diff --git a/index.js b/index.js index b5a83d2..3e0be7e 100644 --- a/index.js +++ b/index.js @@ -276,6 +276,22 @@ function parseDateTimeString (str) { return null } +function parseUrlString (str) { + try { + return new URL(str) + } catch (e) { + return null + } +} + +function parseFilePathString (str) { + const re = /^(?:[A-Za-z]:[\\/]|\\\\|\.{1,2}[\\/]|~[\\/]|\/)/ + if (re.test(str)) { + return str.replace(/\\+/g, '/').replace(/\/+/g, '/') + } + return null +} + function parseExpressionString (str) { if (/^[0-9+\-*/() %.]+$/.test(str) && /[+\-*/()%]/.test(str)) { try { @@ -377,6 +393,11 @@ function parseType (value, type, options = {}) { return new Map(autoParse(value, options)) case 'set': return new Set(autoParse(value, options)) + case 'url': + return new URL(value) + case 'path': + case 'filepath': + return parseFilePathString(String(value)) || String(value) default: if (typeof type === 'function') { if (/Array$/.test(type.name)) { @@ -530,6 +551,14 @@ function autoParse (value, typeOrOptions) { const dt = parseDateTimeString(trimmed) if (dt) return returnIfAllowed(dt, options, originalValue) } + if (options.parseUrls) { + const u = parseUrlString(trimmed) + if (u) return returnIfAllowed(u, options, originalValue) + } + if (options.parseFilePaths) { + const p = parseFilePathString(trimmed) + if (p) return returnIfAllowed(p, options, originalValue) + } value = stripTrimLower(trimmed, Object.assign({}, options, { stripStartChars: false })) if (value === 'undefined' || value === '') { return returnIfAllowed(undefined, options, originalValue) diff --git a/package.json b/package.json index 0992483..492b3d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auto-parse", - "version": "2.2.0", + "version": "2.3.0", "description": "Automatically convert any value to its best matching JavaScript type. Supports numbers, booleans, objects, arrays, BigInt, Symbol, comma-separated numbers, prefix stripping, allowed type enforcement and a plugin API.", "main": "index.js", "types": "index.d.ts", diff --git a/test/performance.test.js b/test/performance.test.js index 9a27fc8..60cd9b0 100644 --- a/test/performance.test.js +++ b/test/performance.test.js @@ -154,4 +154,30 @@ describe('Performance', () => { console.log('date parse time', time) expect(time).toBeLessThan(300) }) + + test('url parse performance', () => { + for (let i = 0; i < 1000; i++) { + autoParse('https://example.com', { parseUrls: true }) + } + const time = benchmark(() => { + for (let i = 0; i < 10000; i++) { + autoParse('https://example.com', { parseUrls: true }) + } + }) + console.log('url parse time', time) + expect(time).toBeLessThan(300) + }) + + test('file path parse performance', () => { + for (let i = 0; i < 1000; i++) { + autoParse('./foo/bar', { parseFilePaths: true }) + } + const time = benchmark(() => { + for (let i = 0; i < 10000; i++) { + autoParse('./foo/bar', { parseFilePaths: true }) + } + }) + console.log('path parse time', time) + expect(time).toBeLessThan(300) + }) }) diff --git a/test/url-path.test.js b/test/url-path.test.js new file mode 100644 index 0000000..b0b5541 --- /dev/null +++ b/test/url-path.test.js @@ -0,0 +1,19 @@ +const autoParse = require('../index.js') +const { assert } = require('chai') + +describe('URL and file path parsing', function () { + it('parses URL strings when enabled', function () { + const url = autoParse('https://example.com', { parseUrls: true }) + assert.instanceOf(url, URL) + assert.strictEqual(url.hostname, 'example.com') + }) + + it('parses file paths when enabled', function () { + const p = autoParse('./foo/bar', { parseFilePaths: true }) + assert.strictEqual(p.includes('foo'), true) + }) + + it('defaults to string when disabled', function () { + assert.strictEqual(autoParse('https://example.com'), 'https://example.com') + }) +})