Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignoreCase flag #122

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions moo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -120,6 +118,7 @@
value: null,
type: null,
shouldThrow: false,
ignoreCase: null,
}

// Avoid Object.assign(), so we support IE9+
Expand Down Expand Up @@ -154,6 +153,7 @@
var fast = Object.create(null)
var fastAllowed = true
var unicodeFlag = null
var ignoreCaseFlag = null
var groups = []
var parts = []

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -257,6 +286,7 @@
var suffix = hasSticky || fallbackRule ? '' : '|'

if (unicodeFlag === true) flags += "u"
nathan marked this conversation as resolved.
Show resolved Hide resolved
if (ignoreCaseFlag === true) flags += "i"
var combined = new RegExp(reUnion(parts) + suffix, flags)
return {regexp: combined, groups: groups, fast: fast, error: errorRule || defaultErrorRule}
}
Expand Down
84 changes: 82 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Expand Down Expand Up @@ -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"})
})

})