Skip to content

Commit 4b0de73

Browse files
committed
Hello, Joe!
0 parents  commit 4b0de73

File tree

9 files changed

+594
-0
lines changed

9 files changed

+594
-0
lines changed

.github/workflows/test.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
pull_request:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: erlef/setup-beam@v1
16+
with:
17+
otp-version: "26.0.2"
18+
gleam-version: "1.6.0-rc1"
19+
rebar3-version: "3"
20+
# elixir-version: "1.15.4"
21+
- run: gleam deps download
22+
- run: gleam test
23+
- run: gleam format --check src test

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.beam
2+
*.ez
3+
/build
4+
erl_crash.dump

README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Regexp
2+
3+
Regular expressions in Gleam!
4+
5+
[![Package Version](https://img.shields.io/hexpm/v/gleam_regexp)](https://hex.pm/packages/gleam_regexp)
6+
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_regexp/)
7+
8+
```sh
9+
gleam add gleam_regexp@1
10+
```
11+
```gleam
12+
import gleam/regexp
13+
14+
pub fn main() {
15+
let assert Ok(re) = regexp.from_string("[0-9]")
16+
17+
regexp.check(re, "abc123")
18+
// -> True
19+
20+
regexp.check(re, "abcxyz")
21+
// -> False
22+
}
23+
```
24+
25+
This package uses the regular expression engine of the underlying platform.
26+
Regular expressions in Erlang and JavaScript largely share the same syntax, but
27+
there are some differences and have different performance characteristics. Be
28+
sure to thoroughly test your code on all platforms that you support when using
29+
this library.
30+
31+
Further documentation can be found at <https://hexdocs.pm/gleam_regexp>.

gleam.toml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name = "gleam_regexp"
2+
version = "1.0.0"
3+
gleam = ">= 1.0.0"
4+
licences = ["Apache-2.0"]
5+
description = "Regular expressions in Gleam!"
6+
7+
repository = { type = "github", user = "gleam-lang", repo = "regexp" }
8+
links = [
9+
{ title = "Sponsor", href = "https://github.com/sponsors/lpil" },
10+
]
11+
12+
[dependencies]
13+
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
14+
15+
[dev-dependencies]
16+
gleeunit = ">= 1.0.0 and < 2.0.0"

manifest.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This file was generated by Gleam
2+
# You typically do not need to edit this file
3+
4+
packages = [
5+
{ name = "gleam_stdlib", version = "0.41.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1B2F80CB1B66B027E3198A2FF71EF3F2F31DF89ED97AD606F25FD387A4C3C1EF" },
6+
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
7+
]
8+
9+
[requirements]
10+
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
11+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

src/gleam/regexp.gleam

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
//// This package uses the regular expression engine of the underlying platform.
2+
//// Regular expressions in Erlang and JavaScript largely share the same syntax, but
3+
//// there are some differences and have different performance characteristics. Be
4+
//// sure to thoroughly test your code on all platforms that you support when using
5+
//// this library.
6+
7+
import gleam/option.{type Option}
8+
9+
pub type Regexp
10+
11+
/// The details about a particular match:
12+
///
13+
pub type Match {
14+
Match(
15+
/// The full string of the match.
16+
content: String,
17+
/// A `Regexp` can have subpatterns, sup-parts that are in parentheses.
18+
submatches: List(Option(String)),
19+
)
20+
}
21+
22+
/// When a regular expression fails to compile:
23+
///
24+
pub type CompileError {
25+
CompileError(
26+
/// The problem encountered that caused the compilation to fail
27+
error: String,
28+
/// The byte index into the string to where the problem was found
29+
/// This value may not be correct in JavaScript environments.
30+
byte_index: Int,
31+
)
32+
}
33+
34+
pub type Options {
35+
Options(case_insensitive: Bool, multi_line: Bool)
36+
}
37+
38+
/// Creates a `Regexp` with some additional options.
39+
///
40+
/// ## Examples
41+
///
42+
/// ```gleam
43+
/// let options = Options(case_insensitive: False, multi_line: True)
44+
/// let assert Ok(re) = compile("^[0-9]", with: options)
45+
/// check(re, "abc\n123")
46+
/// // -> True
47+
/// ```
48+
///
49+
/// ```gleam
50+
/// let options = Options(case_insensitive: True, multi_line: False)
51+
/// let assert Ok(re) = compile("[A-Z]", with: options)
52+
/// check(re, "abc123")
53+
/// // -> True
54+
/// ```
55+
///
56+
pub fn compile(
57+
pattern: String,
58+
with options: Options,
59+
) -> Result(Regexp, CompileError) {
60+
do_compile(pattern, options)
61+
}
62+
63+
@external(erlang, "gleam_regexp_ffi", "compile")
64+
@external(javascript, "../gleam_regexp_ffi.mjs", "compile")
65+
fn do_compile(
66+
pattern: String,
67+
with with: Options,
68+
) -> Result(Regexp, CompileError)
69+
70+
/// Creates a new `Regexp`.
71+
///
72+
/// ## Examples
73+
///
74+
/// ```gleam
75+
/// let assert Ok(re) = from_string("[0-9]")
76+
/// check(re, "abc123")
77+
/// // -> True
78+
/// ```
79+
///
80+
/// ```gleam
81+
/// check(re, "abcxyz")
82+
/// // -> False
83+
/// ```
84+
///
85+
/// ```gleam
86+
/// from_string("[0-9")
87+
/// // -> Error(CompileError(
88+
/// // error: "missing terminating ] for character class",
89+
/// // byte_index: 4
90+
/// // ))
91+
/// ```
92+
///
93+
pub fn from_string(pattern: String) -> Result(Regexp, CompileError) {
94+
compile(pattern, Options(case_insensitive: False, multi_line: False))
95+
}
96+
97+
/// Returns a boolean indicating whether there was a match or not.
98+
///
99+
/// ## Examples
100+
///
101+
/// ```gleam
102+
/// let assert Ok(re) = from_string("^f.o.?")
103+
/// check(with: re, content: "foo")
104+
/// // -> True
105+
/// ```
106+
///
107+
/// ```gleam
108+
/// check(with: re, content: "boo")
109+
/// // -> False
110+
/// ```
111+
///
112+
pub fn check(with regexp: Regexp, content string: String) -> Bool {
113+
do_check(regexp, string)
114+
}
115+
116+
@external(erlang, "gleam_regexp_ffi", "check")
117+
@external(javascript, "../gleam_regexp_ffi.mjs", "check")
118+
fn do_check(regexp: Regexp, string: String) -> Bool
119+
120+
/// Splits a string.
121+
///
122+
/// ## Examples
123+
///
124+
/// ```gleam
125+
/// let assert Ok(re) = from_string(" *, *")
126+
/// split(with: re, content: "foo,32, 4, 9 ,0")
127+
/// // -> ["foo", "32", "4", "9", "0"]
128+
/// ```
129+
///
130+
pub fn split(with regexp: Regexp, content string: String) -> List(String) {
131+
do_split(regexp, string)
132+
}
133+
134+
@external(erlang, "gleam_regexp_ffi", "split")
135+
@external(javascript, "../gleam_regexp_ffi.mjs", "split")
136+
fn do_split(regexp: Regexp, string: String) -> List(String)
137+
138+
/// Collects all matches of the regular expression.
139+
///
140+
/// ## Examples
141+
///
142+
/// ```gleam
143+
/// let assert Ok(re) = from_string("[oi]n a (\\w+)")
144+
/// scan(with: re, content: "I am on a boat in a lake.")
145+
/// // -> [
146+
/// // Match(content: "on a boat", submatches: [Some("boat")]),
147+
/// // Match(content: "in a lake", submatches: [Some("lake")]),
148+
/// // ]
149+
/// ```
150+
///
151+
/// ```gleam
152+
/// let assert Ok(re) = regexp.from_string("([+|\\-])?(\\d+)(\\w+)?")
153+
/// scan(with: re, content: "-36")
154+
/// // -> [
155+
/// // Match(content: "-36", submatches: [Some("-"), Some("36")])
156+
/// // ]
157+
///
158+
/// scan(with: re, content: "36")
159+
/// // -> [
160+
/// // Match(content: "36", submatches: [None, Some("36")])
161+
/// // ]
162+
/// ```
163+
///
164+
/// ```gleam
165+
/// let assert Ok(re) =
166+
/// regexp.from_string("var\\s*(\\w+)\\s*(int|string)?\\s*=\\s*(.*)")
167+
/// scan(with: re, content: "var age = 32")
168+
/// // -> [
169+
/// // Match(
170+
/// // content: "var age = 32",
171+
/// // submatches: [Some("age"), None, Some("32")],
172+
/// // ),
173+
/// // ]
174+
/// ```
175+
///
176+
/// ```gleam
177+
/// let assert Ok(re) = regexp.from_string("let (\\w+) = (\\w+)")
178+
/// scan(with: re, content: "let age = 32")
179+
/// // -> [
180+
/// // Match(
181+
/// // content: "let age = 32",
182+
/// // submatches: [Some("age"), Some("32")],
183+
/// // ),
184+
/// // ]
185+
///
186+
/// scan(with: re, content: "const age = 32")
187+
/// // -> []
188+
/// ```
189+
///
190+
pub fn scan(with regexp: Regexp, content string: String) -> List(Match) {
191+
do_scan(regexp, string)
192+
}
193+
194+
@external(erlang, "gleam_regexp_ffi", "scan")
195+
@external(javascript, "../gleam_regexp_ffi.mjs", "scan")
196+
fn do_scan(regexp: Regexp, string: String) -> List(Match)
197+
198+
/// Creates a new `String` by replacing all substrings that match the regular
199+
/// expression.
200+
///
201+
/// ## Examples
202+
///
203+
/// ```gleam
204+
/// let assert Ok(re) = regexp.from_string("^https://")
205+
/// replace(each: re, in: "https://example.com", with: "www.")
206+
/// // -> "www.example.com"
207+
/// ```
208+
///
209+
/// ```gleam
210+
/// let assert Ok(re) = regexp.from_string("[, +-]")
211+
/// replace(each: re, in: "a,b-c d+e", with: "/")
212+
/// // -> "a/b/c/d/e"
213+
/// ```
214+
@external(erlang, "gleam_regexp_ffi", "replace")
215+
@external(javascript, "../gleam_regexp_ffi.mjs", "replace")
216+
pub fn replace(
217+
each pattern: Regexp,
218+
in string: String,
219+
with substitute: String,
220+
) -> String

src/gleam_regexp_ffi.erl

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-module(gleam_regexp_ffi).
2+
3+
-export([compile/2, check/2, split/2, replace/3, scan/2]).
4+
5+
compile(String, Options) ->
6+
{options, Caseless, Multiline} = Options,
7+
OptionsList = [
8+
unicode,
9+
ucp,
10+
Caseless andalso caseless,
11+
Multiline andalso multiline
12+
],
13+
FilteredOptions = [Option || Option <- OptionsList, Option /= false],
14+
case re:compile(String, FilteredOptions) of
15+
{ok, MP} -> {ok, MP};
16+
{error, {Str, Pos}} ->
17+
{error, {compile_error, unicode:characters_to_binary(Str), Pos}}
18+
end.
19+
20+
check(Regexp, String) ->
21+
re:run(String, Regexp) /= nomatch.
22+
23+
split(Regexp, String) ->
24+
re:split(String, Regexp).
25+
26+
submatches(_, {-1, 0}) -> none;
27+
submatches(String, {Start, Length}) ->
28+
BinarySlice = binary:part(String, {Start, Length}),
29+
case string:is_empty(binary_to_list(BinarySlice)) of
30+
true -> none;
31+
false -> {some, BinarySlice}
32+
end.
33+
34+
matches(String, [{Start, Length} | Submatches]) ->
35+
Submatches1 = lists:map(fun(X) -> submatches(String, X) end, Submatches),
36+
{match, binary:part(String, Start, Length), Submatches1}.
37+
38+
scan(Regexp, String) ->
39+
case re:run(String, Regexp, [global]) of
40+
{match, Captured} -> lists:map(fun(X) -> matches(String, X) end, Captured);
41+
nomatch -> []
42+
end.
43+
44+
replace(Regexp, Subject, Replacement) ->
45+
re:replace(Subject, Regexp, Replacement, [global, {return, binary}]).
46+

0 commit comments

Comments
 (0)