nrser 0.0.26 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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