nrser 0.0.26 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/nrser.rb +1 -0
  3. data/lib/nrser/array.rb +15 -0
  4. data/lib/nrser/binding.rb +7 -1
  5. data/lib/nrser/enumerable.rb +21 -1
  6. data/lib/nrser/errors.rb +56 -6
  7. data/lib/nrser/hash/deep_merge.rb +1 -1
  8. data/lib/nrser/message.rb +33 -0
  9. data/lib/nrser/meta/props.rb +77 -15
  10. data/lib/nrser/meta/props/prop.rb +276 -44
  11. data/lib/nrser/proc.rb +7 -3
  12. data/lib/nrser/refinements/array.rb +5 -0
  13. data/lib/nrser/refinements/enumerable.rb +5 -0
  14. data/lib/nrser/refinements/hash.rb +8 -0
  15. data/lib/nrser/refinements/object.rb +11 -1
  16. data/lib/nrser/refinements/string.rb +17 -3
  17. data/lib/nrser/refinements/symbol.rb +8 -0
  18. data/lib/nrser/refinements/tree.rb +22 -0
  19. data/lib/nrser/rspex.rb +312 -70
  20. data/lib/nrser/rspex/shared_examples.rb +116 -0
  21. data/lib/nrser/string.rb +159 -27
  22. data/lib/nrser/temp/unicode_math.rb +48 -0
  23. data/lib/nrser/text.rb +3 -0
  24. data/lib/nrser/text/indentation.rb +210 -0
  25. data/lib/nrser/text/lines.rb +52 -0
  26. data/lib/nrser/text/word_wrap.rb +29 -0
  27. data/lib/nrser/tree.rb +4 -78
  28. data/lib/nrser/tree/each_branch.rb +76 -0
  29. data/lib/nrser/tree/map_branches.rb +91 -0
  30. data/lib/nrser/tree/map_tree.rb +97 -0
  31. data/lib/nrser/tree/transform.rb +56 -13
  32. data/lib/nrser/types.rb +1 -0
  33. data/lib/nrser/types/array.rb +15 -3
  34. data/lib/nrser/types/is_a.rb +40 -1
  35. data/lib/nrser/types/nil.rb +17 -0
  36. data/lib/nrser/types/paths.rb +17 -2
  37. data/lib/nrser/types/strings.rb +57 -22
  38. data/lib/nrser/types/tuples.rb +5 -0
  39. data/lib/nrser/types/type.rb +47 -6
  40. data/lib/nrser/version.rb +1 -1
  41. data/spec/nrser/errors/abstract_method_error_spec.rb +46 -0
  42. data/spec/nrser/meta/props/to_and_from_data_spec.rb +74 -0
  43. data/spec/nrser/meta/props_spec.rb +6 -2
  44. data/spec/nrser/refinements/erb_spec.rb +100 -1
  45. data/spec/nrser/{common_prefix_spec.rb → string/common_prefix_spec.rb} +9 -0
  46. data/spec/nrser/text/dedent_spec.rb +80 -0
  47. data/spec/nrser/tree/map_branch_spec.rb +83 -0
  48. data/spec/nrser/tree/map_tree_spec.rb +123 -0
  49. data/spec/nrser/tree/transform_spec.rb +26 -29
  50. data/spec/nrser/tree/transformer_spec.rb +179 -0
  51. data/spec/nrser/types/paths_spec.rb +73 -45
  52. data/spec/spec_helper.rb +10 -0
  53. metadata +27 -7
  54. data/spec/nrser/dedent_spec.rb +0 -36
@@ -0,0 +1,116 @@
1
+ # Declarations
2
+ # =======================================================================
3
+
4
+ module NRSER; end
5
+ module NRSER::RSpex; end
6
+
7
+
8
+ # Definitions
9
+ # =======================================================================
10
+
11
+ # Just a namespace module where I stuck shared examples, with lil' utils to
12
+ # alias them to multiple string and symbol names if you like.
13
+ #
14
+ module NRSER::RSpex::SharedExamples
15
+
16
+ # Module (Class/self) Methods (Helpers)
17
+ # =====================================================================
18
+
19
+ # Shitty but simple conversion of natural string names to more symbol-y
20
+ # ones.
21
+ #
22
+ # @example
23
+ # name_to_sym 'expect subject'
24
+ # # => :expect_subject
25
+ #
26
+ # @example
27
+ # name_to_sym 'function'
28
+ # # => :function
29
+ #
30
+ # Doesn't do anything fancy-pants under the hood.
31
+ #
32
+ # @param [String | Symbol] name
33
+ #
34
+ # @return [Symbol]
35
+ #
36
+ def self.name_to_sym name
37
+ name.
38
+ to_s.
39
+ gsub( /\s+/, '_' ).
40
+ gsub( /[^a-zA-Z0-9_]/, '' ).
41
+ downcase.
42
+ to_sym
43
+ end
44
+
45
+
46
+ # Bind a proc as an RSpec shared example to string and symbol names
47
+ def self.bind_names proc, name, prefix: nil
48
+ names = [name.to_s, name_to_sym( name )]
49
+
50
+ unless prefix.nil?
51
+ names << "#{ prefix } #{ name}"
52
+ names << name_to_sym( "#{ prefix }_#{ name}" )
53
+ end
54
+
55
+ names.each do |name|
56
+ shared_examples name, &proc
57
+ end
58
+ end
59
+
60
+
61
+ # Shared Example Blocks and Binding
62
+ # =====================================================================
63
+
64
+ EXPECT_SUBJECT = ->( *expectations ) do
65
+ merge_expectations( *expectations ).each { |state, specs|
66
+ specs.each { |verb, noun|
67
+ it {
68
+ # like: is_expected.to(include(noun))
69
+ is_expected.send state, self.send(verb, noun)
70
+ }
71
+ }
72
+ }
73
+ end # is expected
74
+
75
+ bind_names EXPECT_SUBJECT, "expect subject"
76
+
77
+
78
+ # Shared example for a functional method that compares input and output pairs.
79
+ #
80
+ FUNCTION = ->( mapping: {}, raising: {} ) do
81
+ mapping.each { |args, expected|
82
+ # args = NRSER.as_array args
83
+
84
+ # context "called with #{ args.map( &NRSER::RSpex.method( :short_s ) ).join ', ' }" do
85
+ # subject { super().call *args }
86
+ describe_called_with *args do
87
+
88
+ it {
89
+ expected = unwrap expected, context: self
90
+
91
+ matcher = if expected.respond_to?( :matches? )
92
+ expected
93
+ elsif expected.is_a? NRSER::Message
94
+ expected.send_to self
95
+ else
96
+ eq expected
97
+ end
98
+
99
+ is_expected.to matcher
100
+ }
101
+ end
102
+ }
103
+
104
+ raising.each { |args, error|
105
+ args = NRSER.as_array args
106
+
107
+ context "called with #{ args.map( &NRSER::RSpex.method( :short_s ) ).join ', ' }" do
108
+ # it "rejects #{ args.map( &:inspect ).join ', ' }" do
109
+ it { expect { subject.call *args }.to raise_error( *error ) }
110
+ end
111
+ }
112
+ end
113
+
114
+ bind_names FUNCTION, 'function', prefix: 'a'
115
+
116
+ end # module NRSER::RSpex::SharedExamples
@@ -1,7 +1,20 @@
1
1
  require_relative './string/looks_like'
2
2
 
3
3
  module NRSER
4
+ # @!group String Functions
5
+
6
+ WHITESPACE_RE = /\A[[:space:]]*\z/
7
+
8
+ UNICODE_ELLIPSIS = '…'
9
+
10
+
11
+ def self.whitespace? string
12
+ string =~ WHITESPACE_RE
13
+ end
14
+
15
+
4
16
  class << self
17
+
5
18
  # Functions the operate on strings.
6
19
 
7
20
  # turn a multi-line string into a single line, collapsing whitespace
@@ -17,34 +30,24 @@ module NRSER
17
30
 
18
31
  def common_prefix strings
19
32
  raise ArgumentError.new("argument can't be empty") if strings.empty?
20
- sorted = strings.sort.reject {|line| line == "\n"}
33
+
34
+ sorted = strings.sort
35
+
21
36
  i = 0
37
+
22
38
  while sorted.first[i] == sorted.last[i] &&
23
39
  i < [sorted.first.length, sorted.last.length].min
24
40
  i = i + 1
25
41
  end
26
- strings.first[0...i]
42
+
43
+ sorted.first[0...i]
27
44
  end # common_prefix
28
45
 
29
46
 
30
- def dedent str
31
- return str if str.empty?
32
- lines = str.lines
33
- indent = common_prefix(lines).match(/^\s*/)[0]
34
- return str if indent.empty?
35
- lines.map {|line|
36
- line = line[indent.length..line.length] if line.start_with? indent
37
- }.join
38
- end # dedent
39
-
40
- # I like dedent better, but other libs seems to call it deindent
41
- alias_method :deindent, :dedent
42
-
43
-
44
- def filter_repeated_blank_lines str
47
+ def filter_repeated_blank_lines str, remove_leading: false
45
48
  out = []
46
49
  lines = str.lines
47
- skipping = false
50
+ skipping = remove_leading
48
51
  str.lines.each do |line|
49
52
  if line =~ /^\s*$/
50
53
  unless skipping
@@ -60,16 +63,26 @@ module NRSER
60
63
  end # filter_repeated_blank_lines
61
64
 
62
65
 
63
- # adapted from acrive_support 4.2.0
64
- #
65
- # <https://github.com/rails/rails/blob/7847a19f476fb9bee287681586d872ea43785e53/activesupport/lib/active_support/core_ext/string/indent.rb>
66
- #
67
- def indent str, amount = 2, indent_string=nil, indent_empty_lines=false
68
- indent_string = indent_string || str[/^[ \t]/] || ' '
69
- re = indent_empty_lines ? /^/ : /^(?!$)/
70
- str.gsub(re, indent_string * amount)
71
- end
66
+ def lazy_filter_repeated_blank_lines source, remove_leading: false
67
+ skipping = remove_leading
68
+
69
+ source = source.each_line if source.is_a? String
70
+
71
+ Enumerator::Lazy.new source do |yielder, line|
72
+ if line =~ /^\s*$/
73
+ unless skipping
74
+ yielder << line
75
+ end
76
+ skipping = true
77
+ else
78
+ skipping = false
79
+ yielder << line
80
+ end
81
+ end
82
+
83
+ end # filter_repeated_blank_lines
72
84
 
85
+
73
86
  # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
74
87
  #
75
88
  # 'Once upon a time in a world far far away'.truncate(27)
@@ -108,6 +121,125 @@ module NRSER
108
121
  "#{str[0, stop]}#{omission}"
109
122
  end
110
123
 
124
+
125
+ # Cut the middle out of a string and stick an ellipsis in there instead.
126
+ #
127
+ # @param [String] string
128
+ # Source string.
129
+ #
130
+ # @param [Fixnum] max
131
+ # Max length to allow for the output string.
132
+ #
133
+ # @param [String] omission:
134
+ # The string to stick in the middle where original contents were
135
+ # removed. Defaults to the unicode ellipsis since I'm targeting the CLI
136
+ # at the moment and it saves precious characters.
137
+ #
138
+ # @return [String]
139
+ # String of at most `max` length with the middle chopped out if needed
140
+ # to do so.
141
+ def ellipsis string, max, omission: UNICODE_ELLIPSIS
142
+ return string unless string.length > max
143
+
144
+ trim_to = max - omission.length
145
+
146
+ start = string[0, (trim_to / 2) + (trim_to % 2)]
147
+ finish = string[-( (trim_to / 2) - (trim_to % 2) )..-1]
148
+
149
+ start + omission + finish
150
+ end
151
+
152
+
153
+ # **EXPERIMENTAL!**
154
+ #
155
+ # Try to do "smart" job adding ellipsis to the middle of strings by
156
+ # splitting them by a separator `split` - that defaults to `, ` - then
157
+ # building the result up by bouncing back and forth between tokens at the
158
+ # beginning and end of the string until we reach the `max` length limit.
159
+ #
160
+ # Intended to be used with possibly long single-line strings like
161
+ # `#inspect` returns for complex objects, where tokens are commonly
162
+ # separated by `, `, and producing a reasonably nice result that will fit
163
+ # in a reasonable amount of space, like `rspec` output (which was the
164
+ # motivation).
165
+ #
166
+ # If `string` is already less than `max` then it is just returned.
167
+ #
168
+ # If `string` doesn't contain `split` or just the first and last tokens
169
+ # alone would push the result over `max` then falls back to
170
+ # {NRSER.ellipsis}.
171
+ #
172
+ # If `max` is too small it's going to fall back nearly always... around
173
+ # `64` has seemed like a decent place to start from screwing around on
174
+ # the REPL a bit.
175
+ #
176
+ # @param [String] string
177
+ # Source string.
178
+ #
179
+ # @param [Fixnum] max
180
+ # Max length to allow for the output string. Result will usually be
181
+ # *less* than this unless the fallback to {NRSER.ellipsis} kicks in.
182
+ #
183
+ # @param [String] omission:
184
+ # The string to stick in the middle where original contents were
185
+ # removed. Defaults to the unicode ellipsis since I'm targeting the CLI
186
+ # at the moment and it saves precious characters.
187
+ #
188
+ # @param [String] split:
189
+ # The string to tokenize the `string` parameter by. If you pass a
190
+ # {Regexp} here it might work, it might loop out, maybe.
191
+ #
192
+ # @return [String]
193
+ # String of at most `max` length with the middle chopped out if needed
194
+ # to do so.
195
+ #
196
+ def smart_ellipsis string, max, omission: UNICODE_ELLIPSIS, split: ', '
197
+ return string unless string.length > max
198
+
199
+ unless string.include? split
200
+ return ellipsis string, max, omission: omission
201
+ end
202
+
203
+ tokens = string.split split
204
+
205
+ char_budget = max - omission.length
206
+ start = tokens[0] + split
207
+ finish = tokens[tokens.length - 1]
208
+
209
+ if start.length + finish.length > char_budget
210
+ return ellipsis string, max, omission: omission
211
+ end
212
+
213
+ next_start_index = 1
214
+ next_finish_index = tokens.length - 2
215
+ next_index_is = :start
216
+ next_index = next_start_index
217
+
218
+ while (
219
+ start.length +
220
+ finish.length +
221
+ tokens[next_index].length +
222
+ split.length
223
+ ) <= char_budget do
224
+ if next_index_is == :start
225
+ start += tokens[next_index] + split
226
+ next_start_index += 1
227
+ next_index = next_finish_index
228
+ next_index_is = :finish
229
+ else # == :finish
230
+ finish = tokens[next_index] + split + finish
231
+ next_finish_index -= 1
232
+ next_index = next_start_index
233
+ next_index_is = :start
234
+ end
235
+ end
236
+
237
+ start + omission + finish
238
+
239
+ end # #method_name
240
+
241
+
242
+
111
243
  # Get the constant identified by a string.
112
244
  #
113
245
  # @example
@@ -0,0 +1,48 @@
1
+ module NRSER
2
+ module UnicodeMath
3
+ SET_STARTS = {
4
+ bold: {
5
+ upper: '1D400',
6
+ lower: '1D41A',
7
+ },
8
+
9
+ bold_script: {
10
+ upper: '1D4D0',
11
+ lower: '1D4EA',
12
+ },
13
+ }
14
+
15
+ class CharacterTranslator
16
+ def initialize name, upper_start, lower_start
17
+ @name = name
18
+ @upper_start = upper_start
19
+ @lower_start = lower_start
20
+ end
21
+
22
+ def translate_char char
23
+ upper_offset = char.ord - 'A'.ord
24
+ lower_offset = char.ord - 'a'.ord
25
+
26
+ if upper_offset >= 0 && upper_offset < 26
27
+ [ @upper_start.hex + upper_offset ].pack "U"
28
+ elsif lower_offset >= 0 && lower_offset < 26
29
+ [ @lower_start.hex + lower_offset ].pack "U"
30
+ else
31
+ char
32
+ end
33
+ end
34
+
35
+ def translate string
36
+ string.each_char.map( &method( :translate_char ) ).join
37
+ end
38
+
39
+ alias_method :[], :translate
40
+ end
41
+
42
+ def self.[] name
43
+ name = name.to_sym
44
+ starts = SET_STARTS.fetch name
45
+ CharacterTranslator.new name, starts[:upper], starts[:lower]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ require_relative './text/lines'
2
+ require_relative './text/word_wrap'
3
+ require_relative './text/indentation'
@@ -0,0 +1,210 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+ require_relative './lines'
13
+
14
+
15
+ module NRSER
16
+ # @!group Text
17
+
18
+
19
+ # Constants
20
+ # =====================================================================
21
+
22
+ INDENT_RE = /\A[\ \t]*/
23
+
24
+ INDENT_TAG_MARKER = "\x1E"
25
+ INDENT_TAG_SEPARATOR = "\x1F"
26
+
27
+
28
+ # Functions
29
+ # =====================================================================
30
+
31
+ def self.find_indent text
32
+ common_prefix lines( text ).map { |line| line[INDENT_RE] }
33
+ end
34
+
35
+
36
+ def self.indented? text
37
+ !( find_indent( text ).empty? )
38
+ end
39
+
40
+
41
+ # adapted from active_support 4.2.0
42
+ #
43
+ # <https://github.com/rails/rails/blob/7847a19f476fb9bee287681586d872ea43785e53/activesupport/lib/active_support/core_ext/string/indent.rb>
44
+ #
45
+ def self.indent text,
46
+ amount = 2,
47
+ indent_string: nil,
48
+ indent_empty_lines: false,
49
+ skip_first_line: false
50
+ if skip_first_line
51
+ lines = self.lines text
52
+
53
+ lines.first + indent(
54
+ rest( lines ).join,
55
+ amount,
56
+ indent_string: indent_string,
57
+ skip_first_line: false
58
+ )
59
+
60
+ else
61
+ indent_string = indent_string || text[/^[ \t]/] || ' '
62
+ re = indent_empty_lines ? /^/ : /^(?!$)/
63
+ text.gsub re, indent_string * amount
64
+
65
+ end
66
+ end
67
+
68
+
69
+ def self.dedent text, ignore_whitespace_lines: true
70
+ return text if text.empty?
71
+
72
+ all_lines = text.lines
73
+
74
+ indent_significant_lines = if ignore_whitespace_lines
75
+ all_lines.reject { |line| whitespace? line }
76
+ else
77
+ all_lines
78
+ end
79
+
80
+ indent = find_indent indent_significant_lines
81
+
82
+ return text if indent.empty?
83
+
84
+ all_lines.map { |line|
85
+ if line.start_with? indent
86
+ line[indent.length..-1]
87
+ elsif line.end_with? "\n"
88
+ "\n"
89
+ else
90
+ ""
91
+ end
92
+ }.join
93
+ end # .dedent
94
+
95
+ # I like dedent better, but other libs seems to call it deindent
96
+ singleton_class.send :alias_method, :deindent, :dedent
97
+
98
+
99
+ # Tag each line of `text` with special marker characters around it's leading
100
+ # indent so that the resulting text string can be fed through an
101
+ # interpolation process like ERB that may inject multiline strings and the
102
+ # result can then be fed through {NRSER.indent_untag} to apply the correct
103
+ # indentation to the interpolated lines.
104
+ #
105
+ # Each line of `text` is re-formatted like:
106
+ #
107
+ # "<marker><leading_indent><separator><line_without_leading_indent>"
108
+ #
109
+ # `marker` and `separator` can be configured via keyword arguments, but they
110
+ # default to:
111
+ #
112
+ # - `marker` - {NRSER::INDENT_TAG_MARKER}, the no-printable ASCII
113
+ # *record separator* (ASCII character 30, "\x1E" / "\u001E").
114
+ #
115
+ # - `separator` - {NRSER::INDENT_TAG_SEPARATOR}, the non-printable ASCII
116
+ # *unit separator* (ASCII character 31, "\x1F" / "\u001F")
117
+ #
118
+ # @example With default marker and separator
119
+ # NRSER.indent_tag " hey there!"
120
+ # # => "\x1E \x1Fhey there!"
121
+ #
122
+ # @param [String] text
123
+ # String text to indent tag.
124
+ #
125
+ # @param [String] marker:
126
+ # Special string to mark the start of tagged lines. If interpolated text
127
+ # lines start with this string you're going to have a bad time.
128
+ #
129
+ # @param [String] separator:
130
+ # Special string to separate the leading indent from the rest of the line.
131
+ #
132
+ # @return [String]
133
+ # Tagged text.
134
+ #
135
+ def self.indent_tag text,
136
+ marker: INDENT_TAG_MARKER,
137
+ separator: INDENT_TAG_SEPARATOR
138
+ text.lines.map { |line|
139
+ indent = if match = INDENT_RE.match( line )
140
+ match[0]
141
+ else
142
+ ''
143
+ end
144
+
145
+ "#{ marker }#{ indent }#{ separator }#{ line[indent.length..-1] }"
146
+ }.join
147
+ end # .indent_tag
148
+
149
+
150
+ # Reverse indent tagging that was done via {NRSER.indent_tag}, indenting
151
+ # any untagged lines to the same level as the one above them.
152
+ #
153
+ # @param [String] text
154
+ # Tagged text string.
155
+ #
156
+ # @param [String] marker:
157
+ # Must be the marker used to tag the text.
158
+ #
159
+ # @param [String] separator:
160
+ # Must be the separator used to tag the text.
161
+ #
162
+ # @return [String]
163
+ # Final text with interpolation and indent correction.
164
+ #
165
+ def self.indent_untag text,
166
+ marker: INDENT_TAG_MARKER,
167
+ separator: INDENT_TAG_SEPARATOR
168
+
169
+ current_indent = ''
170
+
171
+ text.lines.map { |line|
172
+ if line.start_with? marker
173
+ current_indent, line = line[marker.length..-1].split( separator, 2 )
174
+ end
175
+
176
+ current_indent + line
177
+
178
+ }.join
179
+
180
+ end # .indent_untag
181
+
182
+
183
+
184
+ # Indent tag a some text via {NRSER.indent_tag}, call the block with it,
185
+ # then pass the result through {NRSER.indent_untag} and return that.
186
+ #
187
+ # @param [String] marker:
188
+ # Special string to mark the start of tagged lines. If interpolated text
189
+ # lines start with this string you're going to have a bad time.
190
+ #
191
+ # @param [String] separator:
192
+ # Must be the separator used to tag the text.
193
+ #
194
+ # @return [String]
195
+ # Final text with interpolation and indent correction.
196
+ #
197
+ def self.with_indent_tagged text,
198
+ marker: INDENT_TAG_MARKER,
199
+ separator: INDENT_TAG_SEPARATOR,
200
+ &interpolate_block
201
+ indent_untag(
202
+ interpolate_block.call(
203
+ indent_tag text, marker: marker, separator: separator
204
+ ),
205
+ marker: marker,
206
+ separator: separator,
207
+ )
208
+ end # .with_indent_tagged
209
+
210
+ end # module NRSER