From ec98d96794e0c08d63057a66d5811c4bd6120e17 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 16 Aug 2021 16:22:58 -0700 Subject: [PATCH] add CaseInsensitiveMap class Note: class name is set to "Map" so that snapshots and inspect output aren't affected. --- lib/case-insensitive-map.js | 48 ++++++++++++++++++++++++++++++++++++ test/case-insensitive-map.js | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 lib/case-insensitive-map.js create mode 100644 test/case-insensitive-map.js diff --git a/lib/case-insensitive-map.js b/lib/case-insensitive-map.js new file mode 100644 index 000000000..8254c3f7a --- /dev/null +++ b/lib/case-insensitive-map.js @@ -0,0 +1,48 @@ +// package children are represented with a Map object, but many file systems +// are case-insensitive and unicode-normalizing, so we need to treat +// node.children.get('FOO') and node.children.get('foo') as the same thing. + +const _keys = Symbol('keys') +const _normKey = Symbol('normKey') +const normalize = s => s.normalize('NFKD').toLowerCase() +const OGMap = Map +module.exports = class Map extends OGMap { + constructor (items = []) { + super() + this[_keys] = new OGMap() + for (const [key, val] of items) + this.set(key, val) + } + + [_normKey] (key) { + return typeof key === 'string' ? normalize(key) : key + } + + get (key) { + const normKey = this[_normKey](key) + return this[_keys].has(normKey) ? super.get(this[_keys].get(normKey)) + : undefined + } + + set (key, val) { + const normKey = this[_normKey](key) + if (this[_keys].has(normKey)) + super.delete(this[_keys].get(normKey)) + this[_keys].set(normKey, key) + return super.set(key, val) + } + + delete (key) { + const normKey = this[_normKey](key) + if (this[_keys].has(normKey)) { + const prevKey = this[_keys].get(normKey) + this[_keys].delete(normKey) + return super.delete(prevKey) + } + } + + has (key) { + const normKey = this[_normKey](key) + return this[_keys].has(normKey) && super.has(this[_keys].get(normKey)) + } +} diff --git a/test/case-insensitive-map.js b/test/case-insensitive-map.js new file mode 100644 index 000000000..a6f9733b3 --- /dev/null +++ b/test/case-insensitive-map.js @@ -0,0 +1,47 @@ +const t = require('tap') +const CMap = require('../lib/case-insensitive-map.js') + +t.test('set values in ctor', t => { + const cmap = new CMap([['a', 'a'], [null, 'null'], [{a: 1}, 'a:1'], ['A', 'A']]) + t.strictSame([...cmap.entries()], [[null, 'null'], [{a: 1}, 'a:1'], ['A', 'A']]) + t.equal(cmap.has('a'), true) + t.equal(cmap.has('A'), true) + t.equal(cmap.get('a'), 'A') + cmap.delete('a') + t.equal(cmap.has('a'), false) + t.equal(cmap.has('A'), false) + t.equal(cmap.get('A'), undefined) + t.end() +}) + +t.test('set values after ctor', t => { + const cmap = new CMap() + cmap.set('a', 'a') + t.equal(cmap.has('a'), true) + t.equal(cmap.has('A'), true) + cmap.set(null, 'null') + cmap.set({a: 1}, 'a:1') + cmap.set('A', 'A') + t.strictSame([...cmap.entries()], [[null, 'null'], [{a: 1}, 'a:1'], ['A', 'A']]) + cmap.delete('a') + t.equal(cmap.has('a'), false) + t.equal(cmap.has('A'), false) + t.equal(cmap.get('A'), undefined) + t.end() +}) + +t.test('dont get confused with undefined or weird values', t => { + const cmap = new CMap() + cmap.set(undefined, 'this is not defined') + cmap.set(NaN, 'this is not a number') + cmap.set('NaN', 'this is a string') + cmap.set('nan', 'this is a quieter string') + + cmap.delete('foo') + cmap.delete('NAN') + t.strictSame([...cmap.entries()], [ + [undefined, 'this is not defined'], + [NaN, 'this is not a number'], + ]) + t.end() +})