/
styling.gleam
230 lines (211 loc) · 6.91 KB
/
styling.gleam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import gleam/option.{type Option, None, Some}
import gleam/list
import gleam/string
import gleam_community/ansi
import gap/comparison.{
type Comparison, type Segments, ListComparison, Match, NoMatch,
StringComparison,
}
import gap/styled_comparison.{type StyledComparison, StyledComparison}
/// The `Highlighter`takes a string representation of the item that was not matching
/// and should return a string representation that can be used to visually indicate that
/// it is a non-matching item.
///
/// The default implementation of the highlighters uses the
/// [gleam_community/ansi](https://hexdocs.pm/gleam_community_ansi/index.html) library
/// to set a different color for the item, but any type if indication can be used as long
/// as it returns a valid string
pub type Highlighter =
fn(String) -> String
/// `Part` is used to indicate to a custom serializer if it should produce a serialization
/// based on a segment with items or the final string that contains already serialized segments
pub type Part(a) {
/// `acc` the already serialized part of the result, `part` is the current segment that should be serialized and appended and `highlighter` is the `Highlighter` that can be used to indicate non-matching items
Part(acc: String, part: List(a), highlight: Highlighter)
/// `all` is a string representing all serialized segments. This can be useful if some string should be prepended/appended to the final result
All(all: String)
}
/// A `Serializer`can be used to create string representation of the comparison results
///
/// See [serialize](#serialize) for adding custom serializers and [mk_generic_serializer](#mk_generic_serializer)
pub type Serializer(a) =
fn(Part(a)) -> String
/// Highlighters to use for indicating matches / non-matches
///
/// `first` is used to highlight non-matches in the first string/list
/// `second` is used to highlight non-matches in the second string/list
/// `matching` is used to highlight matches in the both strings/lists
pub type Highlighters {
Highlighters(first: Highlighter, second: Highlighter, matching: Highlighter)
}
/// Styling of a `Comparison`
///
/// See [from_comparison](#from_comparison)
pub opaque type Styling(a) {
Styling(
comparison: Comparison(a),
serializer: Option(Serializer(a)),
highlight: Option(Highlighters),
)
}
/// Create a new `Styling` from a `Comparison`
///
/// The `Styling` can be customized by adding highlighters and a serializer
/// See [highlight](#highlight) and [serialize](#serialize)
pub fn from_comparison(comparison: Comparison(a)) -> Styling(a) {
Styling(comparison, None, None)
}
/// Add highlighters to the `Styling`
///
/// The highlighters are used to mark the matching/non-matching items in the
/// first/second list/string
pub fn highlight(
styling: Styling(a),
first: Highlighter,
second: Highlighter,
matching: Highlighter,
) -> Styling(a) {
Styling(..styling, highlight: Some(Highlighters(first, second, matching)))
}
/// Add a serializer to the `Styling`
///
/// The serializer is used to create string representation of the items in the segments of the `Comparison`
/// See [Part](#part) for details
///
/// > **NOTE:** `StringComparison` will always use the default string serializer (concatenating the graphemes).
/// > If there is a need for custom serialization of `StringComparison` convert the string to a list of
/// > graphemes and treat it as a `ListComparison`
pub fn serialize(styling: Styling(a), serializer: Serializer(a)) -> Styling(a) {
Styling(..styling, serializer: Some(serializer))
}
/// Creates a styled comparison using either custom highlighters/serializer if they where added or default
/// highlighters and/or serializer
pub fn to_styled_comparison(styling: Styling(a)) -> StyledComparison {
let highlight =
styling.highlight
|> option.unwrap(Highlighters(
first_highlight_default,
second_highlight_default,
no_highlight,
))
case styling.comparison {
StringComparison(first, second) ->
to_strings(
first,
second,
// NOTE: Using string serializer here because otherwise we need to have a specific string serializer on the styling
string_serializer,
highlight.first,
highlight.second,
highlight.matching,
)
ListComparison(first, second) ->
to_strings(
first,
second,
option.unwrap(styling.serializer, generic_serializer),
highlight.first,
highlight.second,
highlight.matching,
)
}
}
/// Default highlighter for the first string/list in the comparison
pub fn first_highlight_default(string: String) -> String {
case string {
" " ->
string
|> ansi.underline()
|> ansi.bold()
|> ansi.green()
_ ->
string
|> ansi.green()
|> ansi.bold()
}
}
/// Default highlighter for the second string/list in the comparison
pub fn second_highlight_default(string: String) -> String {
case string {
" " ->
string
|> ansi.underline()
|> ansi.bold()
|> ansi.red()
_ ->
string
|> ansi.red()
|> ansi.bold()
}
}
/// Default highlighter used for matching items
pub fn no_highlight(string: String) -> String {
string
}
fn string_serializer(part: Part(String)) -> String {
case part {
Part(acc, sequence, highlight) ->
acc
<> {
sequence
|> list.map(highlight)
|> string.join("")
}
All(string) -> string
}
}
fn generic_serializer(part: Part(a)) -> String {
mk_generic_serializer(", ", fn(all) { "[" <> all <> "]" })(part)
}
/// Creates a generic serializer that uses `separator` between all items and calls
/// `around` for possibility to prepend/append strings to the final result
pub fn mk_generic_serializer(separator: String, around: fn(String) -> String) {
fn(part) {
case part {
Part(acc, sequence, highlight) -> {
let segment_separator = case acc {
"" -> ""
_ -> separator
}
acc
<> segment_separator
<> {
sequence
|> list.map(string.inspect)
|> list.map(highlight)
|> string.join(separator)
}
}
All(string) -> around(string)
}
}
}
fn to_strings(
first: Segments(a),
second: Segments(a),
serializer: Serializer(a),
first_highlight: Highlighter,
second_highlight: Highlighter,
no_highlight: Highlighter,
) -> StyledComparison {
let first_styled =
first
|> list.fold("", fn(str, match) {
case match {
Match(item) -> serializer(Part(str, item, no_highlight))
NoMatch(item) -> serializer(Part(str, item, first_highlight))
}
})
let second_styled =
second
|> list.fold("", fn(str, match) {
case match {
Match(item) -> serializer(Part(str, item, no_highlight))
NoMatch(item) -> serializer(Part(str, item, second_highlight))
}
})
StyledComparison(
serializer(All(first_styled)),
serializer(All(second_styled)),
)
}