diff --git a/moo.js b/moo.js index 7418b74..0e9e22d 100644 --- a/moo.js +++ b/moo.js @@ -41,8 +41,6 @@ return '(?:' + reEscape(obj) + ')' } else if (isRegExp(obj)) { - // TODO: consider /u support - if (obj.ignoreCase) throw new Error('RegExp /i flag not allowed') if (obj.global) throw new Error('RegExp /g flag is implied') if (obj.sticky) throw new Error('RegExp /y flag is implied') if (obj.multiline) throw new Error('RegExp /m flag is implied') @@ -120,6 +118,7 @@ value: null, type: null, shouldThrow: false, + ignoreCase: null, } // Avoid Object.assign(), so we support IE9+ @@ -154,6 +153,7 @@ var fast = Object.create(null) var fastAllowed = true var unicodeFlag = null + var ignoreCaseFlag = null var groups = [] var parts = [] @@ -210,9 +210,13 @@ groups.push(options) - // Check unicode flag is used everywhere or nowhere + // Check unicode and ignoreCase flags are used everywhere or nowhere + var hasLiteralsWithCase = false for (var j = 0; j < match.length; j++) { var obj = match[j] + if (typeof obj === "string" && obj.toLowerCase() !== obj.toUpperCase()) { + hasLiteralsWithCase = true + } if (!isRegExp(obj)) { continue } @@ -222,6 +226,31 @@ } else if (unicodeFlag !== obj.unicode) { throw new Error("If one rule is /u then all must be") } + + if (ignoreCaseFlag === null) { + ignoreCaseFlag = obj.ignoreCase + } else if (ignoreCaseFlag !== obj.ignoreCase) { + throw new Error("If one rule is /i then all must be") + } + + // RegExp flags must match the rule's ignoreCase option, if set + if (options.ignoreCase !== null && obj.ignoreCase !== options.ignoreCase) { + throw new Error("ignoreCase option must match RegExp flags (in token '" + options.defaultType + "')") + } + } + + if (hasLiteralsWithCase) { + var ignoreCase = !!options.ignoreCase + if (ignoreCaseFlag === null) { + ignoreCaseFlag = ignoreCase + } else if (ignoreCaseFlag !== ignoreCase) { + if (ignoreCaseFlag) { + throw new Error("Literal must be marked with {ignoreCase: true} (in token '" + options.defaultType + "')") + } else { + // TODO transform literals to ignore case, even if it's not set globally + throw new Error("If one rule sets ignoreCase then all must (in token '" + options.defaultType + "')") + } + } } // convert to RegExp @@ -257,6 +286,7 @@ var suffix = hasSticky || fallbackRule ? '' : '|' if (unicodeFlag === true) flags += "u" + if (ignoreCaseFlag === true) flags += "i" var combined = new RegExp(reUnion(parts) + suffix, flags) return {regexp: combined, groups: groups, fast: fast, error: errorRule || defaultErrorRule} } diff --git a/test/test.js b/test/test.js index 40158f6..d278616 100644 --- a/test/test.js +++ b/test/test.js @@ -28,10 +28,9 @@ describe('compiler', () => { expect(lex4.next()).toMatchObject({type: 'err', text: 'nope!'}) }) - test("warns for /g, /y, /i, /m", () => { + test("warns for /g, /y, /m", () => { expect(() => compile({ word: /foo/ })).not.toThrow() expect(() => compile({ word: /foo/g })).toThrow('implied') - expect(() => compile({ word: /foo/i })).toThrow('not allowed') expect(() => compile({ word: /foo/y })).toThrow('implied') expect(() => compile({ word: /foo/m })).toThrow('implied') }) @@ -1211,3 +1210,84 @@ describe("unicode flag", () => { }) }) + + +describe('ignoreCase flag', () => { + + test("allows all rules to be /i", () => { + expect(() => compile({ a: /foo/i, b: /bar/i })).not.toThrow() + expect(() => compile({ a: /foo/i, b: /bar/ })).toThrow("If one rule is /i then all must be") + expect(() => compile({ a: /foo/, b: /bar/i })).toThrow("If one rule is /i then all must be") + }) + + test("allows all rules to be /ui", () => { + expect(() => compile({ a: /foo/ui, b: /bar/ui })).not.toThrow() + expect(() => compile({ a: /foo/u, b: /bar/i })).toThrow("If one rule is /u then all must be") + expect(() => compile({ a: /foo/i, b: /bar/u })).toThrow("If one rule is /u then all must be") + expect(() => compile({ a: /foo/ui, b: /bar/i })).toThrow("If one rule is /u then all must be") + expect(() => compile({ a: /foo/ui, b: /bar/u })).toThrow("If one rule is /i then all must be") + expect(() => compile({ a: /foo/i, b: /bar/ui })).toThrow("If one rule is /u then all must be") + expect(() => compile({ a: /foo/u, b: /bar/ui })).toThrow("If one rule is /i then all must be") + }) + + test("allow literals to be marked ignoreCase", () => { + expect(() => compile({ + a: /foo/i, + lit: {match: "quxx", ignoreCase: true}, + })).not.toThrow() + expect(() => compile([ + { type: "a", match: /foo/i }, + { type: "lit", match: "quxx", ignoreCase: true }, + ])).not.toThrow() + }) + + test("require literals to be marked ignoreCase", () => { + expect(() => compile({ + a: /foo/i, + lit: "quxx" , + })).toThrow("Literal must be marked with {ignoreCase: true} (in token 'lit')") + expect(() => compile([ + { type: "a", match: /foo/i }, + { type: "lit", match: "quxx" }, + ])).toThrow("Literal must be marked with {ignoreCase: true} (in token 'lit')") + }) + + test("ignoreCase is only required when case is relevant", () => { + expect(() => compile({ + cat: {match: "cat", ignoreCase: true}, + bat: {match: "BAT", ignoreCase: true}, + comma: ',', + semi: ';', + lparen: '(', + rparen: ')', + lbrace: '{', + rbrace: '}', + lbracket: '[', + rbracket: ']', + and: '&&', + or: '||', + bitand: '&', + bitor: '|', + })).not.toThrow() + }) + + test("require ignoreCase option to be match RegExp flags", () => { + expect(() => compile({ + word: { match: /[a-z]+/, ignoreCase: true }, + })).toThrow("ignoreCase option must match RegExp flags") + expect(() => compile({ + word: { match: ["foo", /[a-z]+/], ignoreCase: true }, + })).toThrow("ignoreCase option must match RegExp flags") + expect(() => compile({ + word: { match: /[a-z]+/i, ignoreCase: false }, + })).toThrow("ignoreCase option must match RegExp flags") + }) + + test("supports ignoreCase", () => { + const lexer = compile({ a: /foo/i, b: /bar/i, }) + lexer.reset("FoObAr") + expect(lexer.next()).toMatchObject({value: "FoO"}) + expect(lexer.next()).toMatchObject({value: "bAr"}) + }) + +})