summary refs log tree commit diff stats
path: root/lib/impure/nre.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/impure/nre.nim')
-rw-r--r--lib/impure/nre.nim618
1 files changed, 337 insertions, 281 deletions
diff --git a/lib/impure/nre.nim b/lib/impure/nre.nim
index 6058128dd..39d238055 100644
--- a/lib/impure/nre.nim
+++ b/lib/impure/nre.nim
@@ -6,32 +6,31 @@
 #    distribution, for details about the copyright.
 #
 
-
-from pcre import nil
-import nre.private.util
-import tables
-from strutils import `%`
-from math import ceil
-import options
-from unicode import runeLenAt
-
-export options
-
+when defined(js):
+  {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".}
 
 ## What is NRE?
 ## ============
 ##
 ## A regular expression library for Nim using PCRE to do the hard work.
 ##
-## **Note**: If you love ``sequtils.toSeq`` we have bad news for you. This
-## library doesn't work with it due to documented compiler limitations. As
-## a workaround, use this:
-##
-## .. code-block:: nim
-##
-##    import nre except toSeq
-##
+## For documentation on how to write patterns, there exists `the official PCRE
+## pattern documentation
+## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_. You can also
+## search the internet for a wide variety of third-party documentation and
+## tools.
 ##
+## .. warning:: If you love `sequtils.toSeq` we have bad news for you. This
+##   library doesn't work with it due to documented compiler limitations. As
+##   a workaround, use this:
+runnableExamples:
+  # either `import std/nre except toSeq` or fully qualify `sequtils.toSeq`:
+  import std/sequtils
+  iterator iota(n: int): int =
+    for i in 0..<n: yield i
+  assert sequtils.toSeq(iota(3)) == @[0, 1, 2]
+## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre)
+##   and [regex](https://github.com/nitely/nim-regex).
 ## Licencing
 ## ---------
 ##
@@ -39,47 +38,58 @@ export options
 ## this module.
 ##
 ## .. _`some additional terms`: http://pcre.sourceforge.net/license.txt
-##
-## Example
-## -------
-##
-## .. code-block:: nim
-##
-##     import nre
-##
-##     let vowels = re"[aeoui]"
-##
-##     for match in "moigagoo".findIter(vowels):
-##       echo match.matchBounds
-##     # (a: 1, b: 1)
-##     # (a: 2, b: 2)
-##     # (a: 4, b: 4)
-##     # (a: 6, b: 6)
-##     # (a: 7, b: 7)
-##
-##     let firstVowel = "foo".find(vowels)
-##     let hasVowel = firstVowel.isSome()
-##     if hasVowel:
-##       let matchBounds = firstVowel.get().captureBounds[-1]
-##       echo "first vowel @", matchBounds.get().a
-##       # first vowel @1
+runnableExamples:
+  import std/sugar
+  let vowels = re"[aeoui]"
+  let bounds = collect:
+    for match in "moiga".findIter(vowels): match.matchBounds
+  assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4]
+  from std/sequtils import toSeq
+  let s = sequtils.toSeq("moiga".findIter(vowels))
+    # fully qualified to avoid confusion with nre.toSeq
+  assert s.len == 3
+
+  let firstVowel = "foo".find(vowels)
+  let hasVowel = firstVowel.isSome()
+  assert hasVowel
+  let matchBounds = firstVowel.get().captureBounds[-1]
+  assert matchBounds.a == 1
+
+  # as with module `re`, unless specified otherwise, `start` parameter in each
+  # proc indicates where the scan starts, but outputs are relative to the start
+  # of the input string, not to `start`:
+  assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab"
+  assert find("uxabc", re"ab", start = 3).isNone
+
+from std/pcre import nil
+import nre/private/util
+import std/tables
+from std/strutils import `%`
+import std/options
+from std/unicode import runeLenAt
+
+when defined(nimPreviewSlimSystem):
+  import std/assertions
 
+export options
 
-# Type definitions {{{
 type
   Regex* = ref object
     ## Represents the pattern that things are matched against, constructed with
-    ## ``re(string)``. Examples: ``re"foo"``, ``re(r"(*ANYCRLF)(?x)foo #
-    ## comment".``
+    ## `re(string)`. Examples: `re"foo"`, `re(r"(*ANYCRLF)(?x)foo #
+    ## comment".`
     ##
-    ## ``pattern: string``
-    ##     the string that was used to create the pattern.
+    ## `pattern: string`
+    ## :   the string that was used to create the pattern. For details on how
+    ##     to write a pattern, please see `the official PCRE pattern
+    ##     documentation.
+    ##     <https://www.pcre.org/original/doc/html/pcrepattern.html>`_
     ##
-    ## ``captureCount: int``
-    ##     the number of captures that the pattern has.
+    ## `captureCount: int`
+    ## :   the number of captures that the pattern has.
     ##
-    ## ``captureNameId: Table[string, int]``
-    ##     a table from the capture names to their numeric id.
+    ## `captureNameId: Table[string, int]`
+    ## :   a table from the capture names to their numeric id.
     ##
     ##
     ## Options
@@ -88,30 +98,30 @@ type
     ## The following options may appear anywhere in the pattern, and they affect
     ## the rest of it.
     ##
-    ## -  ``(?i)`` - case insensitive
-    ## -  ``(?m)`` - multi-line: ``^`` and ``$`` match the beginning and end of
+    ## -  `(?i)` - case insensitive
+    ## -  `(?m)` - multi-line: `^` and `$` match the beginning and end of
     ##    lines, not of the subject string
-    ## -  ``(?s)`` - ``.`` also matches newline (*dotall*)
-    ## -  ``(?U)`` - expressions are not greedy by default. ``?`` can be added
+    ## -  `(?s)` - `.` also matches newline (*dotall*)
+    ## -  `(?U)` - expressions are not greedy by default. `?` can be added
     ##    to a qualifier to make it greedy
-    ## -  ``(?x)`` - whitespace and comments (``#``) are ignored (*extended*)
-    ## -  ``(?X)`` - character escapes without special meaning (``\w`` vs.
-    ##    ``\a``) are errors (*extra*)
+    ## -  `(?x)` - whitespace and comments (`#`) are ignored (*extended*)
+    ## -  `(?X)` - character escapes without special meaning (`\w` vs.
+    ##    `\a`) are errors (*extra*)
     ##
     ## One or a combination of these options may appear only at the beginning
     ## of the pattern:
     ##
-    ## -  ``(*UTF8)`` - treat both the pattern and subject as UTF-8
-    ## -  ``(*UCP)`` - Unicode character properties; ``\w`` matches ``я``
-    ## -  ``(*U)`` - a combination of the two options above
-    ## -  ``(*FIRSTLINE*)`` - fails if there is not a match on the first line
-    ## -  ``(*NO_AUTO_CAPTURE)`` - turn off auto-capture for groups;
-    ##    ``(?<name>...)`` can be used to capture
-    ## -  ``(*CR)`` - newlines are separated by ``\r``
-    ## -  ``(*LF)`` - newlines are separated by ``\n`` (UNIX default)
-    ## -  ``(*CRLF)`` - newlines are separated by ``\r\n`` (Windows default)
-    ## -  ``(*ANYCRLF)`` - newlines are separated by any of the above
-    ## -  ``(*ANY)`` - newlines are separated by any of the above and Unicode
+    ## -  `(*UTF8)` - treat both the pattern and subject as UTF-8
+    ## -  `(*UCP)` - Unicode character properties; `\w` matches `я`
+    ## -  `(*U)` - a combination of the two options above
+    ## -  `(*FIRSTLINE*)` - fails if there is not a match on the first line
+    ## -  `(*NO_AUTO_CAPTURE)` - turn off auto-capture for groups;
+    ##    `(?<name>...)` can be used to capture
+    ## -  `(*CR)` - newlines are separated by `\r`
+    ## -  `(*LF)` - newlines are separated by `\n` (UNIX default)
+    ## -  `(*CRLF)` - newlines are separated by `\r\n` (Windows default)
+    ## -  `(*ANYCRLF)` - newlines are separated by any of the above
+    ## -  `(*ANY)` - newlines are separated by any of the above and Unicode
     ##    newlines:
     ##
     ##     single characters VT (vertical tab, U+000B), FF (form feed, U+000C),
@@ -120,8 +130,8 @@ type
     ##     are recognized only in UTF-8 mode.
     ##     —  man pcre
     ##
-    ## -  ``(*JAVASCRIPT_COMPAT)`` - JavaScript compatibility
-    ## -  ``(*NO_STUDY)`` - turn off studying; study is enabled by default
+    ## -  `(*JAVASCRIPT_COMPAT)` - JavaScript compatibility
+    ## -  `(*NO_STUDY)` - turn off studying; study is enabled by default
     ##
     ## For more details on the leading option groups, see the `Option
     ## Setting <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#OPTION_SETTING>`_
@@ -129,7 +139,13 @@ type
     ## Convention <http://man7.org/linux/man-pages/man3/pcresyntax.3.html#NEWLINE_CONVENTION>`_
     ## sections of the `PCRE syntax
     ## manual <http://man7.org/linux/man-pages/man3/pcresyntax.3.html>`_.
-    pattern*: string  ## not nil
+    ##
+    ## Some of these options are not part of PCRE and are converted by nre
+    ## into PCRE flags. These include `NEVER_UTF`, `ANCHORED`,
+    ## `DOLLAR_ENDONLY`, `FIRSTLINE`, `NO_AUTO_CAPTURE`,
+    ## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE wrappers, you
+    ## will need to pass these as separate flags to PCRE.
+    pattern*: string
     pcreObj: ptr pcre.Pcre  ## not nil
     pcreExtra: ptr pcre.ExtraData  ## nil
 
@@ -139,49 +155,40 @@ type
     ## Usually seen as Option[RegexMatch], it represents the result of an
     ## execution. On failure, it is none, on success, it is some.
     ##
-    ## ``pattern: Regex``
-    ##     the pattern that is being matched
+    ## `pattern: Regex`
+    ## :   the pattern that is being matched
     ##
-    ## ``str: string``
-    ##     the string that was matched against
+    ## `str: string`
+    ## :   the string that was matched against
     ##
-    ## ``captures[]: string``
-    ##     the string value of whatever was captured at that id. If the value
-    ##     is invalid, then behavior is undefined. If the id is ``-1``, then
+    ## `captures[]: string`
+    ## :   the string value of whatever was captured at that id. If the value
+    ##     is invalid, then behavior is undefined. If the id is `-1`, then
     ##     the whole match is returned. If the given capture was not matched,
-    ##     ``nil`` is returned.
+    ##     `nil` is returned. See examples for `match`.
     ##
-    ##     -  ``"abc".match(re"(\w)").captures[0] == "a"``
-    ##     -  ``"abc".match(re"(?<letter>\w)").captures["letter"] == "a"``
-    ##     -  ``"abc".match(re"(\w)\w").captures[-1] == "ab"``
+    ## `captureBounds[]: HSlice[int, int]`
+    ## :   gets the bounds of the given capture according to the same rules as
+    ##     the above. If the capture is not filled, then `None` is returned.
+    ##     The bounds are both inclusive.  See examples for `match`.
     ##
-    ## ``captureBounds[]: Option[HSlice[int, int]]``
-    ##     gets the bounds of the given capture according to the same rules as
-    ##     the above. If the capture is not filled, then ``None`` is returned.
-    ##     The bounds are both inclusive.
+    ## `match: string`
+    ## :   the full text of the match.
     ##
-    ##     -  ``"abc".match(re"(\w)").captureBounds[0] == 0 .. 0``
-    ##     -  ``"abc".match(re"").captureBounds[-1] == 0 .. -1``
-    ##     -  ``"abc".match(re"abc").captureBounds[-1] == 0 .. 2``
+    ## `matchBounds: HSlice[int, int]`
+    ## :   the bounds of the match, as in `captureBounds[]`
     ##
-    ## ``match: string``
-    ##     the full text of the match.
+    ## `(captureBounds|captures).toTable`
+    ## :   returns a table with each named capture as a key.
     ##
-    ## ``matchBounds: HSlice[int, int]``
-    ##     the bounds of the match, as in ``captureBounds[]``
+    ## `(captureBounds|captures).toSeq`
+    ## :   returns all the captures by their number.
     ##
-    ## ``(captureBounds|captures).toTable``
-    ##     returns a table with each named capture as a key.
-    ##
-    ## ``(captureBounds|captures).toSeq``
-    ##     returns all the captures by their number.
-    ##
-    ## ``$: string``
-    ##     same as ``match``
+    ## `$: string`
+    ## :   same as `match`
     pattern*: Regex  ## The regex doing the matching.
                      ## Not nil.
     str*: string  ## The string that was matched against.
-                  ## Not nil.
     pcreMatchBounds: seq[HSlice[cint, cint]] ## First item is the bounds of the match
                                             ## Other items are the captures
                                             ## `a` is inclusive start, `b` is exclusive end
@@ -189,7 +196,7 @@ type
   Captures* = distinct RegexMatch
   CaptureBounds* = distinct RegexMatch
 
-  RegexError* = ref object of Exception
+  RegexError* = ref object of CatchableError
 
   RegexInternalError* = ref object of RegexError
     ## Internal error in the module, this probably means that there is a bug
@@ -205,19 +212,71 @@ type
     pattern*: string  ## the pattern that caused the problem
 
   StudyError* = ref object of RegexError
-    ## Thrown when studying the regular expression failes
+    ## Thrown when studying the regular expression fails
     ## for whatever reason. The message contains the error
     ## code.
-# }}}
+
+proc destroyRegex(pattern: Regex) =
+  `=destroy`(pattern.pattern)
+  pcre.free_substring(cast[cstring](pattern.pcreObj))
+  if pattern.pcreExtra != nil:
+    pcre.free_study(pattern.pcreExtra)
+  `=destroy`(pattern.captureNameToId)
 
 proc getinfo[T](pattern: Regex, opt: cint): T =
   let retcode = pcre.fullinfo(pattern.pcreObj, pattern.pcreExtra, opt, addr result)
 
   if retcode < 0:
     # XXX Error message that doesn't expose implementation details
-    raise newException(FieldError, "Invalid getinfo for $1, errno $2" % [$opt, $retcode])
+    raise newException(FieldDefect, "Invalid getinfo for $1, errno $2" % [$opt, $retcode])
+
+proc getNameToNumberTable(pattern: Regex): Table[string, int] =
+  let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT)
+  let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE)
+  let table = cast[ptr UncheckedArray[uint8]](
+                getinfo[int](pattern, pcre.INFO_NAMETABLE))
+
+  result = initTable[string, int]()
+
+  for i in 0 ..< entryCount:
+    let pos = i * entrySize
+    let num = (int(table[pos]) shl 8) or int(table[pos + 1]) - 1
+    var name = ""
+
+    var idx = 2
+    while table[pos + idx] != 0:
+      name.add(char(table[pos + idx]))
+      idx += 1
+
+    result[name] = num
+
+proc initRegex(pattern: string, flags: int, study = true): Regex =
+  new(result, destroyRegex)
+  result.pattern = pattern
+
+  var errorMsg: cstring
+  var errOffset: cint
+
+  result.pcreObj = pcre.compile(cstring(pattern),
+                                # better hope int is at least 4 bytes..
+                                cint(flags), addr errorMsg,
+                                addr errOffset, nil)
+  if result.pcreObj == nil:
+    # failed to compile
+    raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern)
+
+  if study:
+    var options: cint = 0
+    var hasJit: cint
+    if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0:
+      if hasJit == 1'i32:
+        options = pcre.STUDY_JIT_COMPILE
+    result.pcreExtra = pcre.study(result.pcreObj, options, addr errorMsg)
+    if errorMsg != nil:
+      raise StudyError(msg: $errorMsg)
+
+  result.captureNameToId = result.getNameToNumberTable()
 
-# Regex accessors {{{
 proc captureCount*(pattern: Regex): int =
   return getinfo[cint](pattern, pcre.INFO_CAPTURECOUNT)
 
@@ -229,7 +288,7 @@ proc matchesCrLf(pattern: Regex): bool =
   let newlineFlags = flags and (pcre.NEWLINE_CRLF or
                                 pcre.NEWLINE_ANY or
                                 pcre.NEWLINE_ANYCRLF)
-  if newLineFlags > 0u32:
+  if newlineFlags > 0u32:
     return true
 
   # get flags from build config
@@ -244,82 +303,106 @@ proc matchesCrLf(pattern: Regex): bool =
   of -2: return true
   of -1: return true
   else: return false
-# }}}
 
-# Capture accessors {{{
-proc captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(pattern)
 
-proc captures*(pattern: RegexMatch): Captures = return Captures(pattern)
+func captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(pattern)
+
+func captures*(pattern: RegexMatch): Captures = return Captures(pattern)
 
-proc `[]`*(pattern: CaptureBounds, i: int): Option[HSlice[int, int]] =
+func contains*(pattern: CaptureBounds, i: int): bool =
   let pattern = RegexMatch(pattern)
-  if pattern.pcreMatchBounds[i + 1].a != -1:
-    let bounds = pattern.pcreMatchBounds[i + 1]
-    return some(int(bounds.a) .. int(bounds.b-1))
-  else:
-    return none(HSlice[int, int])
+  pattern.pcreMatchBounds[i + 1].a != -1
+
+func contains*(pattern: Captures, i: int): bool =
+  i in CaptureBounds(pattern)
 
-proc `[]`*(pattern: Captures, i: int): string =
+func `[]`*(pattern: CaptureBounds, i: int): HSlice[int, int] =
+  let pattern = RegexMatch(pattern)
+  if not (i in pattern.captureBounds):
+    raise newException(IndexDefect, "Group '" & $i & "' was not captured")
+
+  let bounds = pattern.pcreMatchBounds[i + 1]
+  int(bounds.a)..int(bounds.b-1)
+
+func `[]`*(pattern: Captures, i: int): string =
   let pattern = RegexMatch(pattern)
   let bounds = pattern.captureBounds[i]
 
-  if bounds.isSome:
-    let bounds = bounds.get
-    return pattern.str.substr(bounds.a, bounds.b)
-  else:
-    return nil
+  pattern.str.substr(bounds.a, bounds.b)
 
-proc match*(pattern: RegexMatch): string =
+func match*(pattern: RegexMatch): string =
   return pattern.captures[-1]
 
-proc matchBounds*(pattern: RegexMatch): HSlice[int, int] =
-  return pattern.captureBounds[-1].get
+func matchBounds*(pattern: RegexMatch): HSlice[int, int] =
+  return pattern.captureBounds[-1]
 
-proc `[]`*(pattern: CaptureBounds, name: string): Option[HSlice[int, int]] =
+func contains*(pattern: CaptureBounds, name: string): bool =
   let pattern = RegexMatch(pattern)
-  return pattern.captureBounds[pattern.pattern.captureNameToId.fget(name)]
+  let nameToId = pattern.pattern.captureNameToId
+  if not (name in nameToId):
+      return false
+  nameToId[name] in pattern.captureBounds
 
-proc `[]`*(pattern: Captures, name: string): string =
+func contains*(pattern: Captures, name: string): bool =
+  name in CaptureBounds(pattern)
+
+func checkNamedCaptured(pattern: RegexMatch, name: string) =
+  if not (name in pattern.captureBounds):
+    raise newException(KeyError, "Group '" & name & "' was not captured")
+
+func `[]`*(pattern: CaptureBounds, name: string): HSlice[int, int] =
   let pattern = RegexMatch(pattern)
-  return pattern.captures[pattern.pattern.captureNameToId.fget(name)]
+  checkNamedCaptured(pattern, name)
+  {.noSideEffect.}:
+    result = pattern.captureBounds[pattern.pattern.captureNameToId[name]]
 
-template toTableImpl(cond: untyped) {.dirty.} =
+func `[]`*(pattern: Captures, name: string): string =
+  let pattern = RegexMatch(pattern)
+  checkNamedCaptured(pattern, name)
+  {.noSideEffect.}:
+    result = pattern.captures[pattern.pattern.captureNameToId[name]]
+
+template toTableImpl() {.dirty.} =
   for key in RegexMatch(pattern).pattern.captureNameId.keys:
-    let nextVal = pattern[key]
-    if cond:
-      result[key] = default
-    else:
-      result[key] = nextVal
+    if key in pattern:
+      result[key] = pattern[key]
 
-proc toTable*(pattern: Captures, default: string = nil): Table[string, string] =
+func toTable*(pattern: Captures): Table[string, string] =
   result = initTable[string, string]()
-  toTableImpl(nextVal == nil)
+  toTableImpl()
 
-proc toTable*(pattern: CaptureBounds, default = none(HSlice[int, int])):
-    Table[string, Option[HSlice[int, int]]] =
-  result = initTable[string, Option[HSlice[int, int]]]()
-  toTableImpl(nextVal.isNone)
+func toTable*(pattern: CaptureBounds): Table[string, HSlice[int, int]] =
+  result = initTable[string, HSlice[int, int]]()
+  toTableImpl()
 
-template itemsImpl(cond: untyped) {.dirty.} =
+template itemsImpl() {.dirty.} =
   for i in 0 ..< RegexMatch(pattern).pattern.captureCount:
-    let nextVal = pattern[i]
     # done in this roundabout way to avoid multiple yields (potential code
     # bloat)
-    let nextYieldVal = if cond: default else: nextVal
-    yield nextYieldVal
+    let nextYieldVal = if i in pattern:
+      some(pattern[i])
+    else:
+      default
 
+    yield nextYieldVal
 
-iterator items*(pattern: CaptureBounds, default = none(HSlice[int, int])): Option[HSlice[int, int]] =
-  itemsImpl(nextVal.isNone)
+iterator items*(pattern: CaptureBounds,
+                default = none(HSlice[int, int])): Option[HSlice[int, int]] =
+  itemsImpl()
 
-iterator items*(pattern: Captures, default: string = nil): string =
-  itemsImpl(nextVal == nil)
+iterator items*(pattern: Captures,
+                default: Option[string] = none(string)): Option[string] =
+  itemsImpl()
 
-proc toSeq*(pattern: CaptureBounds, default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] =
-  accumulateResult(pattern.items(default))
+proc toSeq*(pattern: CaptureBounds,
+            default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] =
+  result = @[]
+  for it in pattern.items(default): result.add it
 
-proc toSeq*(pattern: Captures, default: string = nil): seq[string] =
-  accumulateResult(pattern.items(default))
+proc toSeq*(pattern: Captures,
+            default: Option[string] = none(string)): seq[Option[string]] =
+  result = @[]
+  for it in pattern.items(default): result.add it
 
 proc `$`*(pattern: RegexMatch): string =
   return pattern.captures[-1]
@@ -335,10 +418,7 @@ proc `==`*(a, b: Regex): bool =
 proc `==`*(a, b: RegexMatch): bool =
   return a.pattern == b.pattern and
          a.str == b.str
-# }}}
 
-# Creation & Destruction {{{
-# PCRE Options {{{
 const PcreOptions = {
   "NEVER_UTF": pcre.NEVER_UTF,
   "ANCHORED": pcre.ANCHORED,
@@ -394,75 +474,19 @@ proc extractOptions(pattern: string): tuple[pattern: string, flags: int, study:
 
   result.pattern.add pattern[optionStart .. pattern.high]
 
-# }}}
-
-proc destroyRegex(pattern: Regex) =
-  pcre.free_substring(cast[cstring](pattern.pcreObj))
-  pattern.pcreObj = nil
-  if pattern.pcreExtra != nil:
-    pcre.free_study(pattern.pcreExtra)
-
-proc getNameToNumberTable(pattern: Regex): Table[string, int] =
-  let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT)
-  let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE)
-  let table = cast[ptr UncheckedArray[uint8]](
-                getinfo[int](pattern, pcre.INFO_NAMETABLE))
-
-  result = initTable[string, int]()
-
-  for i in 0 ..< entryCount:
-    let pos = i * entrySize
-    let num = (int(table[pos]) shl 8) or int(table[pos + 1]) - 1
-    var name = ""
-
-    var idx = 2
-    while table[pos + idx] != 0:
-      name.add(char(table[pos + idx]))
-      idx += 1
-
-    result[name] = num
-
-proc initRegex(pattern: string, flags: int, study = true): Regex =
-  new(result, destroyRegex)
-  result.pattern = pattern
-
-  var errorMsg: cstring
-  var errOffset: cint
-
-  result.pcreObj = pcre.compile(cstring(pattern),
-                                # better hope int is at least 4 bytes..
-                                cint(flags), addr errorMsg,
-                                addr errOffset, nil)
-  if result.pcreObj == nil:
-    # failed to compile
-    raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern)
-
-  if study:
-    var options: cint = 0
-    var hasJit: cint
-    if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0:
-      if hasJit == 1'i32:
-        options = pcre.STUDY_JIT_COMPILE
-    result.pcreExtra = pcre.study(result.pcreObj, options, addr errorMsg)
-    if errorMsg != nil:
-      raise StudyError(msg: $errorMsg)
-
-  result.captureNameToId = result.getNameToNumberTable()
-
 proc re*(pattern: string): Regex =
   let (pattern, flags, study) = extractOptions(pattern)
   initRegex(pattern, flags, study)
-# }}}
 
-# Operations {{{
 proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Option[RegexMatch] =
-  var myResult = RegexMatch(pattern : pattern, str : str)
+  var myResult = RegexMatch(pattern: pattern, str: str)
   # See PCRE man pages.
   # 2x capture count to make room for start-end pairs
   # 1x capture count as slack space for PCRE
   let vecsize = (pattern.captureCount() + 1) * 3
   # div 2 because each element is 2 cints long
-  myResult.pcreMatchBounds = newSeq[HSlice[cint, cint]](ceil(vecsize / 2).int)
+  # plus 1 because we need the ceiling, not the floor
+  myResult.pcreMatchBounds = newSeq[HSlice[cint, cint]]((vecsize + 1) div 2)
   myResult.pcreMatchBounds.setLen(vecsize div 3)
 
   let strlen = if endpos == int.high: str.len else: endpos+1
@@ -483,33 +507,46 @@ proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Opt
     of pcre.ERROR_NOMATCH:
       return none(RegexMatch)
     of pcre.ERROR_NULL:
-      raise newException(AccessViolationError, "Expected non-null parameters")
+      raise newException(AccessViolationDefect, "Expected non-null parameters")
     of pcre.ERROR_BADOPTION:
-      raise RegexInternalError(msg : "Unknown pattern flag. Either a bug or " &
+      raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " &
         "outdated PCRE.")
     of pcre.ERROR_BADUTF8, pcre.ERROR_SHORTUTF8, pcre.ERROR_BADUTF8_OFFSET:
-      raise InvalidUnicodeError(msg : "Invalid unicode byte sequence",
-        pos : myResult.pcreMatchBounds[0].a)
+      raise InvalidUnicodeError(msg: "Invalid unicode byte sequence",
+        pos: myResult.pcreMatchBounds[0].a)
     else:
-      raise RegexInternalError(msg : "Unknown internal error: " & $execRet)
+      raise RegexInternalError(msg: "Unknown internal error: " & $execRet)
 
 proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
-  ## Like ```find(...)`` <#proc-find>`_, but anchored to the start of the
-  ## string. This means that ``"foo".match(re"f") == true``, but
-  ## ``"foo".match(re"o") == false``.
+  ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the
+  ## string.
+  runnableExamples:
+    assert "foo".match(re"f").isSome
+    assert "foo".match(re"o").isNone
+
+    assert "abc".match(re"(\w)").get.captures[0] == "a"
+    assert "abc".match(re"(?<letter>\w)").get.captures["letter"] == "a"
+    assert "abc".match(re"(\w)\w").get.captures[-1] == "ab"
+
+    assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0
+    assert 0 in "abc".match(re"(\w)").get.captureBounds
+    assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1
+    assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2
   return str.matchImpl(pattern, start, endpos, pcre.ANCHORED)
 
 iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): RegexMatch =
-  ## Works the same as ```find(...)`` <#proc-find>`_, but finds every
-  ## non-overlapping match. ``"2222".find(re"22")`` is ``"22", "22"``, not
-  ## ``"22", "22", "22"``.
-  ##
-  ## Arguments are the same as ```find(...)`` <#proc-find>`_
+  ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every
+  ## non-overlapping match:
+  runnableExamples:
+    import std/sugar
+    assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"]
+     # not @["22", "22", "22"]
+  ## Arguments are the same as `find(...)<#find,string,Regex,int>`_
   ##
   ## Variants:
   ##
-  ## -  ``proc findAll(...)`` returns a ``seq[string]``
-  # see pcredemo for explanation
+  ## -  `proc findAll(...)` returns a `seq[string]`
+  # see pcredemo for explanation => https://www.pcre.org/original/doc/html/pcredemo.html
   let matchesCrLf = pattern.matchesCrLf()
   let unicode = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS) and
     pcre.UTF8) > 0u32
@@ -530,7 +567,7 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R
       # either the end of the input or the string
       # cannot be split here - we also need to bail
       # if we've never matched and we've already tried to...
-      if offset >= strlen or neverMatched:
+      if flags == 0 or offset >= strlen or neverMatched: # All matches found
         break
 
       if matchesCrLf and offset < (str.len - 1) and
@@ -546,19 +583,18 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R
     else:
       neverMatched = false
       offset = match.get.matchBounds.b + 1
-
       yield match.get
 
 proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
   ## Finds the given pattern in the string between the end and start
   ## positions.
   ##
-  ## ``start``
-  ##     The start point at which to start matching. ``|abc`` is ``0``;
-  ##     ``a|bc`` is ``1``
+  ## `start`
+  ## :   The start point at which to start matching. `|abc` is `0`;
+  ##     `a|bc` is `1`
   ##
-  ## ``endpos``
-  ##     The maximum index for a match; ``int.high`` means the end of the
+  ## `endpos`
+  ## :   The maximum index for a match; `int.high` means the end of the
   ##     string, otherwise it’s an inclusive upper bound.
   return str.matchImpl(pattern, start, endpos, 0)
 
@@ -570,29 +606,33 @@ proc findAll*(str: string, pattern: Regex, start = 0, endpos = int.high): seq[st
 proc contains*(str: string, pattern: Regex, start = 0, endpos = int.high): bool =
   ## Determine if the string contains the given pattern between the end and
   ## start positions:
-  ## -  "abc".contains(re"bc") == true
-  ## -  "abc".contains(re"cd") == false
-  ## -  "abc".contains(re"a", start = 1) == false
-  ##
-  ## Same as ``isSome(str.find(pattern, start, endpos))``.
+  ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`.
+  runnableExamples:
+    assert "abc".contains(re"bc")
+    assert not "abc".contains(re"cd")
+    assert not "abc".contains(re"a", start = 1)
+
   return isSome(str.find(pattern, start, endpos))
 
 proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] =
   ## Splits the string with the given regex. This works according to the
-  ## rules that Perl and Javascript use:
-  ##
-  ## -  If the match is zero-width, then the string is still split:
-  ##    ``"123".split(r"") == @["1", "2", "3"]``.
-  ##
-  ## -  If the pattern has a capture in it, it is added after the string
-  ##    split: ``"12".split(re"(\d)") == @["", "1", "", "2", ""]``.
+  ## rules that Perl and Javascript use.
   ##
-  ## -  If ``maxsplit != -1``, then the string will only be split
-  ##    ``maxsplit - 1`` times. This means that there will be ``maxsplit``
-  ##    strings in the output seq.
-  ##    ``"1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"]``
+  ## `start` behaves the same as in `find(...)<#find,string,Regex,int>`_.
   ##
-  ## ``start`` behaves the same as in ```find(...)`` <#proc-find>`_.
+  runnableExamples:
+    # -  If the match is zero-width, then the string is still split:
+    assert "123".split(re"") == @["1", "2", "3"]
+
+    # -  If the pattern has a capture in it, it is added after the string
+    #    split:
+    assert "12".split(re"(\d)") == @["", "1", "", "2", ""]
+
+    # -  If `maxsplit != -1`, then the string will only be split
+    #    `maxsplit - 1` times. This means that there will be `maxsplit`
+    #    strings in the output seq.
+    assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"]
+
   result = @[]
   var lastIdx = start
   var splits = 0
@@ -620,7 +660,8 @@ proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string]
 
     for cap in match.captures:
       # if there are captures, include them in the result
-      result.add(cap)
+      if cap.isSome:
+        result.add(cap.get)
 
     if splits == maxSplit - 1:
       break
@@ -654,27 +695,27 @@ template replaceImpl(str: string, pattern: Regex,
 
 proc replace*(str: string, pattern: Regex,
               subproc: proc (match: RegexMatch): string): string =
-  ## Replaces each match of Regex in the string with ``sub``, which should
-  ## never be or return ``nil``.
+  ## Replaces each match of Regex in the string with `subproc`, which should
+  ## never be or return `nil`.
   ##
-  ## If ``sub`` is a ``proc (RegexMatch): string``, then it is executed with
+  ## If `subproc` is a `proc (RegexMatch): string`, then it is executed with
   ## each match and the return value is the replacement value.
   ##
-  ## If ``sub`` is a ``proc (string): string``, then it is executed with the
-  ## full text of the match and and the return value is the replacement
-  ## value.
+  ## If `subproc` is a `proc (string): string`, then it is executed with the
+  ## full text of the match and the return value is the replacement value.
   ##
-  ## If ``sub`` is a string, the syntax is as follows:
+  ## If `subproc` is a string, the syntax is as follows:
   ##
-  ## -  ``$$`` - literal ``$``
-  ## -  ``$123`` - capture number ``123``
-  ## -  ``$foo`` - named capture ``foo``
-  ## -  ``${foo}`` - same as above
-  ## -  ``$1$#`` - first and second captures
-  ## -  ``$#`` - first capture
-  ## -  ``$0`` - full match
+  ## -  `$$` - literal `$`
+  ## -  `$123` - capture number `123`
+  ## -  `$foo` - named capture `foo`
+  ## -  `${foo}` - same as above
+  ## -  `$1$#` - first and second captures
+  ## -  `$#` - first capture
+  ## -  `$0` - full match
   ##
-  ## If a given capture is missing, a ``ValueError`` exception is thrown.
+  ## If a given capture is missing, `IndexDefect` thrown for un-named captures
+  ## and `KeyError` for named captures.
   replaceImpl(str, pattern, subproc(match))
 
 proc replace*(str: string, pattern: Regex,
@@ -686,10 +727,25 @@ proc replace*(str: string, pattern: Regex, sub: string): string =
   replaceImpl(str, pattern,
     formatStr(sub, match.captures[name], match.captures[id - 1]))
 
-# }}}
-
-let SpecialCharMatcher = re"([\\+*?[^\]$(){}=!<>|:-])"
-proc escapeRe*(str: string): string =
-  ## Escapes the string so it doesn’t match any special characters.
-  ## Incompatible with the Extra flag (``X``).
-  str.replace(SpecialCharMatcher, "\\$1")
+proc escapeRe*(str: string): string {.gcsafe.} =
+  ## Escapes the string so it doesn't match any special characters.
+  ## Incompatible with the Extra flag (`X`).
+  ##
+  ## Escaped char: `\ + * ? [ ^ ] $ ( ) { } = ! < > | : -`
+  runnableExamples:
+    assert escapeRe("fly+wind") == "fly\\+wind"
+    assert escapeRe("!") == "\\!"
+    assert escapeRe("nim*") == "nim\\*"
+
+  #([\\+*?[^\]$(){}=!<>|:-])
+  const SpecialCharMatcher = {'\\', '+', '*', '?', '[', '^', ']', '$', '(',
+                              ')', '{', '}', '=', '!', '<', '>', '|', ':',
+                              '-'}
+
+  for c in items(str):
+    case c
+    of SpecialCharMatcher:
+      result.add("\\")
+      result.add(c)
+    else:
+      result.add(c)