// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
struct Regexp {
instructions : Array[Instruction]
map : Map[String, Int]
capture : Int
allow_exponentiaion : Bool
}
///|
/// Regular expression parsing error types
///
/// Defines various error conditions that may be encountered during parsing
pub enum Err {
InternalError // Internal error
InvalidCharClass // Invalid character class
InvalidEscape // Invalid escape sequence
InvalidNamedCapture // Invalid named capture group
InvalidRepeatOp // Invalid repeat operator
InvalidRepeatSize // Invalid repeat count
MissingBracket // Missing right bracket ]
MissingParenthesis // Missing right parenthesis )
MissingRepeatArgument // Missing repeat argument
TrailingBackslash // Trailing backslash
UnexpectedParenthesis // Unexpected parenthesis
} derive(Show)
///|
/// Parsing error exception type
///
/// Contains error information and related string view context
pub suberror RegexpError {
RegexpError(err~ : Err, source_fragment~ : StringView)
} derive(Show)
///|
/// Compiles a regex into a matcher.
fn ParseResult::compile(self : Self) -> Regexp {
{
instructions: self.ast.compile(),
map: self.capture_map,
capture: self.captures,
allow_exponentiaion: self.has_backreference,
}
}
///|
/// Compiles a regular expression into an execution engine.
///
/// Parses and compiles a regular expression string into a reusable execution engine.
/// The compiled engine can be executed multiple times, avoiding the overhead of repeated parsing and compilation.
///
/// Args:
/// - `regexp`: A string view of the regular expression.
///
/// Returns: The compiled regular expression execution engine.
///
/// Throws: `Error_` if the regular expression syntax is invalid.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("a(bc|de)f")
/// inspect(
/// engine.execute("xxabcf").results(),
/// content=(
/// #|[Some("abcf"), Some("bc")]
/// ),
/// )
/// }
/// ```
pub fn compile(
regexp : StringView,
flags? : StringView = "",
) -> Regexp raise RegexpError {
parse(regexp, flags={
multiline: flags.contains("m"),
singleline: flags.contains("s"),
ignore_case: flags.contains("i"),
}).compile() catch {
@parse.RegexpError(err~, source_fragment~) => {
let err = match err {
@parse.Err::InternalError => Err::InternalError
@parse.Err::InvalidCharClass => Err::InvalidCharClass
@parse.Err::InvalidEscape => Err::InvalidEscape
@parse.Err::InvalidNamedCapture => Err::InvalidNamedCapture
@parse.Err::InvalidRepeatOp => Err::InvalidRepeatOp
@parse.Err::InvalidRepeatSize => Err::InvalidRepeatSize
@parse.Err::MissingBracket => Err::MissingBracket
@parse.Err::MissingParenthesis => Err::MissingParenthesis
@parse.Err::MissingRepeatArgument => Err::MissingRepeatArgument
@parse.Err::TrailingBackslash => Err::TrailingBackslash
@parse.Err::UnexpectedParenthesis => Err::UnexpectedParenthesis
}
raise RegexpError(err~, source_fragment~)
}
}
}
///|
/// Gets the index of a capture group by its name.
///
/// Finds the index of a capture group with the specified name in the results.
/// Used to access named capture groups, such as those defined in the form `(?pattern)`.
///
/// Args:
/// - `self`: The regular expression execution engine.
/// - `name`: The name of the capture group.
///
/// Returns: The index of the capture group, or `None` if the group name does not exist.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("(?\\w+)")
/// inspect(engine.group_by_name("word"), content="Some(1)")
/// }
/// ```
pub fn Regexp::group_by_name(self : Regexp, name : String) -> Int? {
self.map.get(name)
}
///|
/// Gets the total number of capture groups in the regular expression.
///
/// Returns the number of capture groups defined in the regular expression pattern, including both named and anonymous groups.
/// Group with index 0 always represents the entire match, and actual capture groups start from index 1.
///
/// Args:
/// - `self`: The regular expression execution engine.
///
/// Returns: The total number of capture groups (including the full match at index 0).
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("(a)(b|c)")
/// inspect(engine.group_count(), content="3")
/// }
/// ```
pub fn Regexp::group_count(self : Regexp) -> Int {
self.capture
}
///|
/// Gets the names of all named capture groups in the regular expression.
///
/// Returns an iterator that contains the names of all named capture groups in the regular expression.
/// Only explicitly named capture groups, such as those defined in the form `(?pattern)`, are returned.
///
/// Args:
/// - `self`: The regular expression execution engine.
///
/// Returns: An iterator containing all group names.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("(?a)(?b)")
/// inspect(
/// engine.group_names(),
/// content=(
/// #|["first", "second"]
/// ),
/// )
/// }
/// ```
pub fn Regexp::group_names(self : Regexp) -> Array[String] {
self.map.keys().collect()
}
///|
/// Executes a regular expression match on the input text.
///
/// Uses the compiled regular expression engine to perform a match on the specified input text,
/// returning a `MatchResult` object containing the match results and capture group information.
/// It uses a leftmost-first matching strategy, returning the first match found.
///
/// Args:
/// - `self`: The regular expression execution engine.
/// - `input`: The input text to match against.
///
/// Returns: A `MatchResult` object containing the match status and capture group information.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("a(bc|de)f")
/// let result = engine.execute("xxabcf")
/// if result.matched() {
/// inspect(
/// result.get(0),
/// content=(
/// #|Some("abcf")
/// ),
/// )
/// }
/// }
/// ```
pub fn Regexp::execute(self : Regexp, input : StringView) -> MatchResult {
let captures = vm(
self.instructions,
input,
self.capture,
allow_exponentiaion=self.allow_exponentiaion,
)
let (before, after) = if captures is [0..<_ as start, 0..<_ as end, ..] {
(input.view(end_offset=start), input.view(start_offset=end))
} else {
(input, input.view(start_offset=input.length()))
}
{ input, captures, names: self.map, before, after }
}
///|
/// Executes a regular expression match on the input text.
///
/// Uses the compiled regular expression engine to perform a match on the specified input text,
/// returning a `MatchResult` object containing the match results and capture group information.
/// It uses a leftmost-first matching strategy, returning the first match found.
///
/// Args:
/// - `self`: The regular expression execution engine.
/// - `input`: The input text to match against.
///
/// Returns: A tuple of
/// - `MatchResult` object containing the match status and capture group information.
/// - `StringView` representing the remaining text after the match.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("a(bc|de)f")
/// let result = engine.execute("abcfxx")
/// guard result.matched()
/// inspect(result.before(), content="")
/// inspect(result.after(), content="xx")
/// let result = engine.execute("axxf")
/// guard !result.matched()
/// inspect(result.before(), content="axxf")
/// inspect(result.after(), content="")
/// }
/// ```
#deprecated("Use `Regexp::execute` and `MatchResult::before`/`after` instead.")
pub fn Regexp::execute_with_remainder(
self : Regexp,
input : StringView,
) -> (MatchResult, StringView) {
let captures = vm(
self.instructions,
input,
self.capture,
allow_exponentiaion=self.allow_exponentiaion,
)
if captures is [0..<_ as start, 0..<_ as end, ..] {
(
{
input,
captures,
names: self.map,
before: input.view(end_offset=start),
after: input.view(start_offset=end),
},
input.view(start_offset=end),
)
} else {
(
{
input,
captures,
names: self.map,
before: input,
after: input.view(start_offset=input.length()),
},
input,
)
}
}
///|
/// Executes a regular expression match on the input text.
///
/// Uses the compiled regular expression engine to perform a match on the specified input text,
/// returning a `MatchResult` object containing the match results and capture group information.
/// It uses a leftmost-first matching strategy, returning the first match found.
///
/// Args:
/// - `self`: The regular expression execution engine.
/// - `input`: The input text to match against.
///
/// Returns: A tuple of
/// - `MatchResult` object containing the match status and capture group information.
///
/// Example:
/// ```moonbit check
/// test {
/// let engine = compile("a(bc|de)f")
/// guard engine.match_("abcfxx") is Some(result)
/// inspect(result.after(), content="xx")
/// assert_true(engine.match_("axxf") is None)
/// }
/// ```
pub fn Regexp::match_(self : Regexp, input : StringView) -> MatchResult? {
let captures = vm(
self.instructions,
input,
self.capture,
allow_exponentiaion=self.allow_exponentiaion,
)
guard captures is [0..<_ as start, 0..<_ as end, ..] else { return None }
let before = input.view(end_offset=start)
let after = input.view(start_offset=end)
Some({ input, captures, names: self.map, before, after })
}
///|
/// Regular expression match result.
///
/// Contains the complete result information of a regular expression match, including:
/// - The original input text.
/// - An array of capture group position information.
/// - A map of named capture groups.
///
/// Structure of the capture group array:
/// - Index 0, 1: Start and end positions of the full match.
/// - Index 2, 3: Start and end positions of the first capture group.
/// - Index 4, 5: Start and end positions of the second capture group.
/// - And so on...
struct MatchResult {
input : StringView
captures : Array[Int]
names : Map[String, Int]
before : StringView
after : StringView
}
///|
/// Checks if the match was successful.
///
/// Determines whether the regular expression found a match in the input text.
/// An empty capture group array indicates that no match was found.
///
/// Args:
/// - `self`: The match result object.
///
/// Returns: `true` if a match was found, otherwise `false`.
///
/// Example:
/// ```moonbit check
/// test {
/// let result = compile("text").execute("text")
/// inspect(result.matched(), content="true")
/// }
/// ```
pub fn MatchResult::matched(self : Self) -> Bool {
not(self.captures is [])
}
///|
/// Gets the content of a capture group by its index.
///
/// Returns the corresponding matched text based on the capture group's index.
/// Index 0 represents the full match, while indices 1 and above represent individual capture groups.
///
/// Args:
/// - `self`: The match result object.
/// - `index`: The index of the capture group (0 for the full match).
///
/// Returns:
/// - `Some(view)`: The text view corresponding to the capture group.
/// - `None`: If the index is invalid or the capture group did not match.
///
/// Example:
/// ```moonbit check
/// test {
/// let result = compile(".(bc)").execute("abc")
/// inspect(
/// result.get(0),
/// content=(
/// #|Some("abc")
/// ),
/// )
/// let first_group = result.get(1)
/// inspect(
/// first_group,
/// content=(
/// #|Some("bc")
/// ),
/// )
/// }
/// ```
pub fn MatchResult::get(self : Self, index : Int) -> StringView? {
if self.captures.is_empty() ||
index < 0 ||
index >= self.captures.length() / 2 {
return None
}
let start = self.captures[index * 2]
let end = self.captures[index * 2 + 1]
if start < 0 || end < start || end > self.input.length() {
return None
}
Some(self.input.view(start_offset=start, end_offset=end))
}
///|
/// Returns a map containing all named capture groups and their matched
/// contents.
///
/// Parameters:
///
/// * `self` : The match result object containing named capture group
/// information.
///
/// Returns a map where keys are the names of capture groups and values are
/// their corresponding matched content. If a named group did not participate in
/// the match or was not matched, its value will be `None`.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let engine = compile("(?\\w+)\\s+(?\\d+)")
/// let result = engine.execute("hello 123")
/// inspect(
/// result.groups(),
/// content=(
/// #|{"word": "hello", "num": "123"}
/// ),
/// )
/// }
/// ```
///
pub fn MatchResult::groups(self : Self) -> Map[String, StringView] {
self.names
.iter()
.filter_map(p => if self.get(p.1) is Some(content) {
Some((p.0, content))
} else {
None
})
|> Map::from_iter
}
///|
/// Gets an iterator over the content of all valid capture groups.
///
/// Returns an iterator that contains the content of all successfully matched capture groups,
/// including the full match (index 0) and all other capture groups. Only valid matched content is returned,
/// skipping any unmatched capture groups.
///
/// Args:
/// - `self`: The match result object.
///
/// Returns: An iterator containing the content of all valid capture groups.
///
/// Example:
/// ```moonbit check
/// test {
/// let result = compile("(\\w)+").execute("abc")
/// inspect(
/// result.results(),
/// content=(
/// #|[Some("abc"), Some("c")]
/// ),
/// )
/// }
/// ```
pub fn MatchResult::results(self : Self) -> Array[StringView?] {
let results = []
for i = 0; i < self.captures.length() / 2; i = i + 1 {
let start = self.captures[i * 2]
let end = self.captures[i * 2 + 1]
if start == -1 && end == -1 {
results.push(None)
} else {
results.push(Some(self.input.view(start_offset=start, end_offset=end)))
}
}
results
}
///|
/// Gets the text before the match.
///
/// If the match was not successful, this will return the entire input text.
///
/// ```moonbit check
/// test {
/// let engine = compile("world")
/// let result = engine.execute("hello world")
/// inspect(result.before(), content="hello ")
/// let result = engine.execute("say hello")
/// inspect(result.before(), content="say hello")
/// }
/// ```
pub fn MatchResult::before(self : Self) -> StringView {
self.before
}
///|
/// Gets the remaining text after the match.
///
/// If the match was not successful, this will return the empty string.
///
/// ```moonbit check
/// test {
/// let engine = compile("hello")
/// let result = engine.execute("hello world")
/// inspect(result.after(), content=" world")
/// let result = engine.execute("say yes")
/// inspect(result.after(), content="")
/// }
/// ```
pub fn MatchResult::after(self : Self) -> StringView {
self.after
}
///|
test "rest method" {
let engine = compile("hello")
let result = engine.execute("hello world")
inspect(result.matched(), content="true")
inspect(result.after(), content=" world")
let result2 = engine.execute("say hello")
inspect(result2.matched(), content="true")
inspect(result2.after(), content="")
let result3 = engine.execute("hi there")
inspect(result3.matched(), content="false")
inspect(result3.before(), content="hi there")
inspect("Test rest method completed", content="Test rest method completed")
}
///|
test "rest method with capture groups" {
let engine = compile("(\\w+)\\s+")
let result = engine.execute("hello world test")
inspect(result.matched(), content="true")
inspect(
result.get(0),
content=(
#|Some("hello ")
),
)
inspect(
result.get(1),
content=(
#|Some("hello")
),
)
inspect(result.after(), content="world test")
let engine2 = compile(".*")
let result2 = engine2.execute("everything")
inspect(result2.matched(), content="true")
inspect(
result2.get(0),
content=(
#|Some("everything")
),
)
inspect(result2.after(), content="")
}
///|
test "rest method usage example" {
let engine = compile("\\b\\w+\\b")
let mut input : StringView = "word1 word2 word3"
let result1 = engine.execute(input)
inspect(
result1.get(0),
content=(
#|Some("word1")
),
)
input = result1.after()
let result2 = engine.execute(input)
inspect(
result2.get(0),
content=(
#|Some("word2")
),
)
inspect(result2.after(), content=" word3")
}