···
1
1
import gleam/list
2
2
-
import gleam/string
3
2
4
3
type State {
5
4
Text
···
28
27
@external(javascript, "../../string_width_ffi.mjs", "match_ansi")
29
28
pub fn match(str: String) -> List(#(Int, Int)) {
30
29
let #(_, _, matches) = {
31
31
-
use #(pos, state, matches), cp <- fold_codepoints(str, #(0, Text, []))
30
30
+
use #(pos, state, matches), byte <- fold_bytes(str, #(0, Text, []))
32
31
33
32
case state {
34
33
Text ->
35
35
-
case cp {
34
34
+
case byte {
36
35
// ESC
37
36
0x1b -> #(pos + 1, Escape(pos), matches)
38
38
-
// C1 CSI
39
39
-
0x9b -> #(pos + 1, CSI(pos), matches)
40
40
-
// C1 OSC
41
41
-
0x9d -> #(pos + 1, OSC(pos), matches)
42
37
_ -> #(pos + 1, state, matches)
43
38
}
44
39
45
40
Escape(start) ->
46
46
-
case cp {
41
41
+
case byte {
47
42
// '['
48
43
0x5b -> #(pos + 1, CSI(start), matches)
49
44
// ']'
50
45
0x5d -> #(pos + 1, OSC(start), matches)
51
46
// a..z single character end
52
52
-
_ if cp >= 0x61 && cp <= 0x7a -> {
47
47
+
_ if byte >= 0x61 && byte <= 0x7a -> {
53
48
#(pos + 1, Text, [#(start, pos - start + 1), ..matches])
54
49
}
55
50
// A..Z single character end
56
56
-
_ if cp >= 0x41 && cp <= 0x5a -> {
51
51
+
_ if byte >= 0x41 && byte <= 0x5a -> {
57
52
#(pos + 1, Text, [#(start, pos - start + 1), ..matches])
58
53
}
59
54
// ignore (use OSC state)
···
61
56
}
62
57
63
58
CSI(start) ->
64
64
-
case cp {
59
59
+
case byte {
65
60
// csi parameter or intermediate
66
66
-
_ if cp >= 0x20 && cp <= 0x3f -> #(pos + 1, state, matches)
61
61
+
_ if byte >= 0x20 && byte <= 0x3f -> #(pos + 1, state, matches)
67
62
// csi final range
68
68
-
_ if cp >= 0x40 && cp <= 0x7e -> {
63
63
+
_ if byte >= 0x40 && byte <= 0x7e -> {
69
64
#(pos + 1, Text, [#(start, pos - start + 1), ..matches])
70
65
}
71
66
// invalid sequence
···
73
68
}
74
69
75
70
OSC(start) ->
76
76
-
case cp {
71
71
+
case byte {
77
72
// BEL, ST
78
78
-
0x07 | 0x9c -> #(pos + 1, Text, [#(start, pos - start + 1), ..matches])
73
73
+
0x07 -> #(pos + 1, Text, [#(start, pos - start + 1), ..matches])
79
74
// ESC
80
75
0x1b -> #(pos + 1, OSCEsc(start), matches)
81
76
_ -> #(pos + 1, state, matches)
82
77
}
83
78
84
79
OSCEsc(start) ->
85
85
-
case cp {
80
80
+
case byte {
86
81
// \\, BEL, ST
87
87
-
0x5c | 0x07 | 0x9c -> {
82
82
+
0x5c | 0x07 -> {
88
83
#(pos + 1, Text, [#(start, pos - start + 1), ..matches])
89
84
}
90
85
_ -> #(pos + 1, OSC(start), matches)
···
95
90
list.reverse(matches)
96
91
}
97
92
98
98
-
// Duplicates from string_width, but making them public does weird things to
99
99
-
// codegen as far as I can remember and we want to be SURE we can go fast.
100
100
-
101
101
-
// also, this makes it quite self-contained if i ever want to publish this.
102
102
-
103
103
-
@external(erlang, "string_width_ffi", "fold_codepoints")
104
104
-
@external(javascript, "../../string_width_ffi.mjs", "fold_codepoints")
105
105
-
fn fold_codepoints(
106
106
-
str: String,
107
107
-
state: state,
108
108
-
fun: fn(state, Int) -> state,
109
109
-
) -> state {
110
110
-
use state, cp <- list.fold(string.to_utf_codepoints(str), state)
111
111
-
fun(state, string.utf_codepoint_to_int(cp))
112
112
-
}
93
93
+
@external(erlang, "string_width_ffi", "fold_bytes")
94
94
+
@external(javascript, "../../string_width_ffi.mjs", "fold_bytes")
95
95
+
fn fold_bytes(str: String, state: state, fun: fn(state, Int) -> state) -> state
113
96
114
97
@external(erlang, "erlang", "split_binary")
115
98
@external(javascript, "../../string_width_ffi.mjs", "unsafe_split")
···
1
1
-module(string_width_ffi).
2
2
-export([
3
3
+
fold_bytes/3,
3
4
fold_codepoints/3,
4
5
utf_codepoint_to_string/1,
5
6
get_terminal_size/0
6
7
]).
7
8
8
8
-
fold_codepoints(Binary, State, Fun) ->
9
9
-
case Binary of
10
10
-
<<Cp/utf8, Rest/binary>> -> fold_codepoints(Rest, Fun(State, Cp), Fun);
11
11
-
_ -> State
12
12
-
end.
9
9
+
fold_bytes(<<Byte/integer, Rest/binary>>, State, Fun) -> fold_bytes(Rest, Fun(State, Byte), Fun);
10
10
+
fold_bytes(_Binary, State, _Fun) -> State.
11
11
+
12
12
+
fold_codepoints(<<Cp/utf8, Rest/binary>>, State, Fun) -> fold_codepoints(Rest, Fun(State, Cp), Fun);
13
13
+
fold_codepoints(_Binary, State, _Fun) -> State.
13
14
14
14
-
utf_codepoint_to_string(Cp) ->
15
15
-
<<Cp/utf8>>.
15
15
+
utf_codepoint_to_string(Cp) -> <<Cp/utf8>>.
16
16
17
17
get_terminal_size() ->
18
18
case {io:rows(), io:columns()} of
···
1
1
import { toList, Ok, Error } from './gleam.mjs'
2
2
3
3
-
const ANSI_RE = /(?:\x1b[a-zA-Z])|(?:(?:\x1b\[|\x9b)[\x20-\x3f]*?[\x40-\x7e])|(?:[\x1b\x0d].*?(?:\x07|\x1b\\|\x9c))/mg
3
3
+
const ANSI_RE = /\x1b(?:[a-zA-Z]|(?:\[[\x20-\x3f]*?[\x40-\x7e])|(?:.*?(?:\x07|\x1b\\)))/mg
4
4
const ASCII_RE = /[\x20-\x7e]+(?=[\x20-\x7e]|$)/g;
5
5
const SEGMENTER = new Intl.Segmenter();
6
6
···
70
70
71
71
export function unsafe_split(str, at) {
72
72
return [str.slice(0, at), str.slice(at)]
73
73
+
}
74
74
+
75
75
+
export function fold_bytes(str, state, fun) {
76
76
+
for (let i = 0; i < str.length; ++i) {
77
77
+
state = fun(state, str.charCodeAt(i))
78
78
+
}
79
79
+
return state
73
80
}
74
81
75
82
export function fold_codepoints(str, state, fun) {
···
43
43
}
44
44
45
45
pub fn match_link_test() {
46
46
-
use st <- list.each(["\u{07}", "\u{1b}\u{5c}", "\u{9c}"])
47
47
-
let l = string.length(st)
46
46
+
use st <- list.each(["\u{07}", "\u{1b}\u{5c}"])
47
47
+
let l = string.byte_size(st)
48
48
ansi.match(
49
49
"\u{001B}]8;k=v;https://example-a.com/?a_b=1&c=2#tit%20le"
50
50
<> st
···
94
94
ansi.strip("wibble\u{001B}[00;38;5;244mwobble")
95
95
|> should.equal("wibblewobble")
96
96
}
97
97
+
98
98
+
pub fn emoji_test() {
99
99
+
ansi.strip(
100
100
+
"π©βπ©βπ¦βπ¦\u{1b}[00;38;5;244mhello\u{1b}[mπ©βπ©βπ¦βπ¦",
101
101
+
)
102
102
+
|> should.equal("π©βπ©βπ¦βπ¦helloπ©βπ©βπ¦βπ¦")
103
103
+
}