@@ -177,43 +177,79 @@ class RegexParser(pattern: String) {
177177 base
178178 }
179179
180+ private def parseFlags (): RegexFlagSet = {
181+ def parseFlagSet (): Set [RegexFlag ] = {
182+ var result = Set .empty[RegexFlag ]
183+ while (! eof() && peek().exists(ch => RegexFlag .allFlagsString.contains(ch))) {
184+ result += RegexFlag .fromChar(consume())
185+ }
186+ result
187+ }
188+ val flags = parseFlagSet()
189+ val negated = if (peek().contains('-' )) {
190+ consumeExpected('-' ); parseFlagSet()
191+ } else Set .empty[RegexFlag ]
192+ RegexFlagSet (flags, negated)
193+ }
194+
180195 private def parseGroup (): RegexAST = {
181- val groupType = if (pos + 1 < pattern.length
182- && pattern.charAt(pos) == '?'
183- && " :!=<>" .contains(pattern.charAt(pos+ 1 ))) {
196+ val groupType = if (peek().contains('?' )) {
184197 consumeExpected('?' )
185- consume() match { // guaranteed exhaustive by the contains call above
186- case ':' => RegexGroup .NonCapturing
187- case '!' => RegexGroup .NegativeLookahead
188- case '=' => RegexGroup .PositiveLookahead
189- case '>' => RegexGroup .Independent
190- case '<' if pos < pattern.length => consume() match {
191- case '!' => RegexGroup .NegativeLookbehind
192- case '=' => RegexGroup .PositiveLookbehind
193- case ch if isLetter(ch) =>
194- val nameStart = pos- 1
195- while (! eof() && peek().exists(c => isLetter(c) || isAsciiDigit(c))) {
196- skip()
197- }
198- val name = pattern.substring(nameStart, pos)
199- if (! peek().contains('>' )) {
200- throw new RegexUnsupportedException (
201- " Illegal named capture group: malformed <name>" , Some (nameStart- 1 ))
202- }
203- consumeExpected('>' )
204- RegexGroup .Named (name)
205- case _ => throw new RegexUnsupportedException (
206- " Unexpected character after '<' in group" , Some (pos- 1 ))
198+ peek() match {
199+ case None => throw new RegexUnsupportedException (" Unterminated inline flags" , Some (pos))
200+ case Some (ch) if " :!=<>" .contains(ch) => consumeExpected(ch) match {
201+ // guaranteed exhaustive by the contains call above
202+ case ':' => RegexGroup .NonCapturing
203+ case '!' => RegexGroup .NegativeLookahead
204+ case '=' => RegexGroup .PositiveLookahead
205+ case '>' => RegexGroup .Independent
206+ case '<' if pos < pattern.length => consume() match {
207+ case '!' => RegexGroup .NegativeLookbehind
208+ case '=' => RegexGroup .PositiveLookbehind
209+ case ch if isLetter(ch) =>
210+ val nameStart = pos- 1
211+ while (! eof() && peek().exists(c => isLetter(c) || isAsciiDigit(c))) {
212+ skip()
213+ }
214+ val name = pattern.substring(nameStart, pos)
215+ if (! peek().contains('>' )) {
216+ throw new RegexUnsupportedException (
217+ " Illegal named capture group: malformed <name>" , Some (nameStart- 1 ))
218+ }
219+ consumeExpected('>' )
220+ RegexGroup .Named (name)
221+ case _ => throw new RegexUnsupportedException (
222+ " Unexpected character after '<' in group" , Some (pos- 1 ))
223+ }
224+ case '<' => throw new RegexUnsupportedException (
225+ " Pattern may not end with trailing '<' in group" , Some (pos- 1 ))
207226 }
208- case '<' => throw new RegexUnsupportedException (
209- " Pattern may not end with trailing '<' in group" , Some (pos- 1 ))
227+ case Some (_) =>
228+ val flags = parseFlags()
229+ // Leave the ':' to distinguish scoped flags from inline flags.
230+ peek() match {
231+ case Some (':' ) | Some (')' ) | None =>
232+ case Some (ch) =>
233+ throw new RegexUnsupportedException (s " Unexpected inline flag ' $ch' " , Some (pos))
234+ }
235+ RegexGroup .ScopedFlags (flags)
210236 }
211237 } else {
212238 RegexGroup .Capturing
213239 }
214- val term = parseUntil(() => peek().contains(')' ))
215- consumeExpected(')' )
216- RegexGroup (groupType, term)
240+ (peek(), groupType) match {
241+ case (None , RegexGroup .ScopedFlags (_)) =>
242+ throw new RegexUnsupportedException (" Unterminated inline flags" , Some (pos))
243+ case (None , _) => throw new RegexUnsupportedException (" Unclosed group" , Some (pos))
244+ case (Some (')' ), RegexGroup .ScopedFlags (flags)) =>
245+ consumeExpected(')' )
246+ RegexInlineFlags (flags)
247+ case (Some (ch), _) =>
248+ if (ch == ':' ) consumeExpected(':' )
249+ val term = parseUntil(() => peek().contains(')' ))
250+ consumeExpected(')' )
251+ RegexGroup (groupType, term)
252+ }
217253 }
218254
219255 private def parseCharacterClass (): RegexCharacterClass = {
@@ -1633,6 +1669,8 @@ class CudfRegexTranspiler(mode: RegexMode) {
16331669 " Independent groups are not supported"
16341670 case RegexGroup .Named (_) =>
16351671 " Named capture groups are not supported"
1672+ case RegexGroup .ScopedFlags (_) =>
1673+ " Scoped inline flags are not supported"
16361674 case _ =>
16371675 s " Unknown group type: ${g.groupType}"
16381676 }
@@ -1779,6 +1817,35 @@ sealed case class RegexSequence(parts: ListBuffer[RegexAST]) extends RegexAST {
17791817 override def toRegexString : String = parts.map(_.toRegexString).mkString
17801818}
17811819
1820+ sealed abstract class RegexFlag (val char : Char )
1821+ object RegexFlag {
1822+ case object CaseInsensitive extends RegexFlag ('i' )
1823+ case object UnixLines extends RegexFlag ('d' )
1824+ case object Multiline extends RegexFlag ('m' )
1825+ case object DotAll extends RegexFlag ('s' )
1826+ case object UnicodeCase extends RegexFlag ('u' )
1827+ case object Comments extends RegexFlag ('x' )
1828+ case object UnicodeClasses extends RegexFlag ('U' )
1829+ final val all : Seq [RegexFlag ] =
1830+ Seq (CaseInsensitive , UnixLines , Multiline , DotAll , UnicodeCase , Comments , UnicodeClasses )
1831+ final val fromChar : Map [Char , RegexFlag ] = all.map(f => f.char -> f).toMap
1832+ final val allFlagsString : String = all.map(_.char).mkString
1833+ }
1834+
1835+ sealed case class RegexFlagSet (flags : Set [RegexFlag ], negated : Set [RegexFlag ]) {
1836+ def isEmpty : Boolean = flags.isEmpty && negated.isEmpty
1837+ def toRegexString : String = {
1838+ val flagsString = flags.map(_.char).mkString
1839+ val negatedString = if (negated.isEmpty) " " else s " - ${negated.map(_.char).mkString}"
1840+ s " ${flagsString}${negatedString}"
1841+ }
1842+ }
1843+
1844+ sealed case class RegexInlineFlags (flags : RegexFlagSet ) extends RegexAST {
1845+ override def children (): Seq [RegexAST ] = Seq .empty
1846+ override def toRegexString : String = s " (? ${flags.toRegexString}) "
1847+ }
1848+
17821849object RegexGroup {
17831850 sealed trait Type
17841851 case object Capturing extends Type
@@ -1789,6 +1856,7 @@ object RegexGroup {
17891856 case object NegativeLookbehind extends Type
17901857 case class Named (name : String ) extends Type
17911858 case object Independent extends Type
1859+ case class ScopedFlags (flags : RegexFlagSet ) extends Type
17921860}
17931861
17941862sealed case class RegexGroup (groupType : RegexGroup .Type , term : RegexAST ) extends RegexAST {
@@ -1807,6 +1875,7 @@ sealed case class RegexGroup(groupType: RegexGroup.Type, term: RegexAST) extends
18071875 case NegativeLookbehind => s " (?<! ${term.toRegexString}) "
18081876 case Named (name) => s " (?< $name> ${term.toRegexString}) "
18091877 case Independent => s " (?> ${term.toRegexString}) "
1878+ case ScopedFlags (flags) => s " (? ${flags.toRegexString}: ${term.toRegexString}) "
18101879 }
18111880}
18121881
0 commit comments