opal-activesupport 0.3.1 → 0.3.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +4 -1
- data/Rakefile +22 -6
- data/lib/opal/activesupport/version.rb +1 -1
- data/opal-activesupport.gemspec +1 -0
- data/opal/active_support/concern.rb +152 -0
- data/opal/active_support/core_ext/string.rb +1 -38
- data/opal/active_support/core_ext/string/filters.rb +146 -0
- data/opal/active_support/core_ext/string/inflections.rb +66 -4
- data/opal/active_support/inflections.rb +2 -0
- data/opal/active_support/inflector.rb +5 -2
- data/opal/active_support/inflector/inflections.rb +64 -66
- data/opal/active_support/inflector/methods.rb +198 -0
- data/test/abstract_unit.rb +22 -5
- data/test/concern_test.rb +144 -0
- data/test/constantize_test_cases.rb +121 -0
- data/test/core_ext/numeric_ext_test.rb +1 -1
- data/test/core_ext/string_ext_test.rb +675 -304
- data/test/inflector_test.rb +578 -0
- data/test/inflector_test_cases.rb +91 -31
- metadata +27 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b194f0cee109c7a300653dc94711e9a9ce0b2d5d99e83f6ef48c23b3e126c48
|
4
|
+
data.tar.gz: 32d323abd0065e8b6f3c577959403f27cc630e11c9ffa3e69991f1b7d6b6b30e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80fdb487f5bc4ccedd111843959084276649820c8f308871b54c005049589aa26c78b975674c2cecb44fbc627931d3852e5fe11696d4a2868e3f7657b8113b14
|
7
|
+
data.tar.gz: 1d08de6d0ae15c2f2c043f72e3941e8e9355f53c3e476c1947d21001ee273a9dd4563fd71dbdba51f91cb5442e6511b5191cd7fecd37d0b29e1688f1825e5d53
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
### 0.3.2 - unreleased
|
2
|
+
|
3
|
+
[Changes](https://github.com/opal/opal-activesupport/compare/v0.3.2...HEAD)
|
4
|
+
|
5
|
+
### 0.3.2 - 2019-04-27
|
6
|
+
|
7
|
+
[Changes](https://github.com/opal/opal-activesupport/compare/v0.3.1...v0.3.2)
|
8
|
+
|
9
|
+
- Added `ActiveSupport::Concern` (#19).
|
10
|
+
- Added `String#truncate` (#22).
|
11
|
+
- Properly backported: (#21).
|
12
|
+
+ `ActiveSupport::Inflector.pluralize` and `String#pluralize`
|
13
|
+
+ `ActiveSupport::Inflector.singularize` and `String#singularize`
|
14
|
+
+ `ActiveSupport::Inflector.constantize` and `String#constantize`
|
15
|
+
+ `ActiveSupport::Inflector.safe_constantize` and `String#safe_constantize`
|
16
|
+
+ `ActiveSupport::Inflector.camelize` and `String#camelize`
|
17
|
+
+ `ActiveSupport::Inflector.titleize` and `String#titleize`
|
18
|
+
+ `ActiveSupport::Inflector.underscore` and `String#underscore`
|
19
|
+
+ `ActiveSupport::Inflector.dasherize` and `String#dasherize`
|
20
|
+
+ `ActiveSupport::Inflector.demodulize` and `String#demodulize`
|
21
|
+
+ `ActiveSupport::Inflector.deconstantize` and `String#deconstantize`
|
22
|
+
+ `ActiveSupport::Inflector.tableize` and `String#tableize`
|
23
|
+
+ `ActiveSupport::Inflector.classify` and `String#classify`
|
24
|
+
+ `ActiveSupport::Inflector.humanize` and `String#humanize`
|
25
|
+
+ `ActiveSupport::Inflector.upcase_first` and `String#upcase_first`
|
26
|
+
+ `ActiveSupport::Inflector.foreign_key` and `String#foreign_key`
|
27
|
+
|
28
|
+
### 0.3.1 - 2018-01-28
|
29
|
+
|
30
|
+
[Changes](https://github.com/opal/opal-activesupport/compare/v0.3.0...v0.3.1)
|
31
|
+
|
32
|
+
- Fix `Inflections.irregular`
|
33
|
+
- Fix `Inflector.apply_inflections`
|
34
|
+
- Fix `Inflector.inflections`
|
35
|
+
- Fix `return` handling of x-string for Opal v0.11
|
36
|
+
|
37
|
+
### 0.3.0 - 2015-12-23
|
38
|
+
|
39
|
+
[Changes](https://github.com/opal/opal-activesupport/compare/v0.2.0...v0.3.0)
|
40
|
+
|
41
|
+
### 0.2.0 - 2015-10-08
|
42
|
+
|
43
|
+
[Changes](https://github.com/opal/opal-activesupport/compare/v0.1.0...v0.2.0)
|
44
|
+
|
45
|
+
### 0.1.0 - 2015-02-03
|
46
|
+
|
47
|
+
_the fogs of the past 🌫_
|
data/README.md
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# Opal: ActiveSupport
|
2
2
|
|
3
|
-
|
3
|
+
[](https://travis-ci.org/opal/opal-activesupport)
|
4
|
+
|
4
5
|
> @AstonJ But it's vanilla Ruby. It's not like you have ActiveSupport available, which somewhat defeats it for me.
|
5
6
|
|
7
|
+
_[@dhh 6:44 PM - Oct 23, 2012](https://twitter.com/dhh/status/260783823254601728)_
|
8
|
+
|
6
9
|
|
7
10
|
## Installation
|
8
11
|
|
data/Rakefile
CHANGED
@@ -1,15 +1,31 @@
|
|
1
1
|
require 'bundler'
|
2
2
|
Bundler.require
|
3
|
+
require 'bundler/gem_tasks'
|
3
4
|
|
4
|
-
require 'opal/minitest/rake_task'
|
5
|
-
# Opal::Minitest::RakeTask.new
|
6
5
|
|
7
6
|
task :test do
|
7
|
+
require 'opal'
|
8
|
+
require 'opal/cli_runners'
|
9
|
+
require 'opal/minitest'
|
10
|
+
|
8
11
|
Opal::Config.arity_check_enabled = true
|
9
|
-
|
10
|
-
|
12
|
+
Opal::Config.dynamic_require_severity = :warning
|
13
|
+
|
14
|
+
Opal.append_path 'opal'
|
15
|
+
Opal.append_path 'test'
|
16
|
+
|
17
|
+
builder = Opal::Builder.new
|
18
|
+
builder.build 'opal'
|
19
|
+
builder.build 'opal/platform'
|
20
|
+
builder.build 'minitest'
|
21
|
+
Dir['test/**/*_test.rb'].map do |file|
|
22
|
+
builder.build file.sub(%r{^test/}, '')
|
23
|
+
end
|
24
|
+
builder.build_str 'Minitest.run', 'minitest-runner.rb'
|
25
|
+
|
26
|
+
runner_name = ENV['RUNNER'] || 'nodejs'
|
27
|
+
runner_class = Opal::CliRunners.const_get(runner_name.capitalize)
|
28
|
+
runner_class.new(output: $stdout).run(builder.to_s, [])
|
11
29
|
end
|
12
30
|
|
13
31
|
task default: :test
|
14
|
-
|
15
|
-
require 'bundler/gem_tasks'
|
data/opal-activesupport.gemspec
CHANGED
@@ -0,0 +1,152 @@
|
|
1
|
+
module ActiveSupport
|
2
|
+
# A typical module looks like this:
|
3
|
+
#
|
4
|
+
# module M
|
5
|
+
# def self.included(base)
|
6
|
+
# base.extend ClassMethods
|
7
|
+
# base.class_eval do
|
8
|
+
# scope :disabled, -> { where(disabled: true) }
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# module ClassMethods
|
13
|
+
# ...
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
18
|
+
# written as:
|
19
|
+
#
|
20
|
+
# require 'active_support/concern'
|
21
|
+
#
|
22
|
+
# module M
|
23
|
+
# extend ActiveSupport::Concern
|
24
|
+
#
|
25
|
+
# included do
|
26
|
+
# scope :disabled, -> { where(disabled: true) }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class_methods do
|
30
|
+
# ...
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
35
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
36
|
+
# following:
|
37
|
+
#
|
38
|
+
# module Foo
|
39
|
+
# def self.included(base)
|
40
|
+
# base.class_eval do
|
41
|
+
# def self.method_injected_by_foo
|
42
|
+
# ...
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# module Bar
|
49
|
+
# def self.included(base)
|
50
|
+
# base.method_injected_by_foo
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# class Host
|
55
|
+
# include Foo # We need to include this dependency for Bar
|
56
|
+
# include Bar # Bar is the module that Host really needs
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
60
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
61
|
+
#
|
62
|
+
# module Bar
|
63
|
+
# include Foo
|
64
|
+
# def self.included(base)
|
65
|
+
# base.method_injected_by_foo
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# class Host
|
70
|
+
# include Bar
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# Unfortunately this won't work, since when +Foo+ is included, its base
|
74
|
+
# is the +Bar+ module, not the +Host+ class. With ActiveSupport::Concern,
|
75
|
+
# module dependencies are properly resolved:
|
76
|
+
#
|
77
|
+
# require 'active_support/concern'
|
78
|
+
#
|
79
|
+
# module Foo
|
80
|
+
# extend ActiveSupport::Concern
|
81
|
+
# included do
|
82
|
+
# def self.method_injected_by_foo
|
83
|
+
# ...
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# module Bar
|
89
|
+
# extend ActiveSupport::Concern
|
90
|
+
# include Foo
|
91
|
+
#
|
92
|
+
# included do
|
93
|
+
# self.method_injected_by_foo
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# class Host
|
98
|
+
# include Bar # It works, now Bar takes care of its dependencies
|
99
|
+
# end
|
100
|
+
module Concern
|
101
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
102
|
+
# Opal 0.11 always passes an argument to Exception.exception
|
103
|
+
def initialize(_)
|
104
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.extended(base) #:nodoc:
|
109
|
+
base.instance_variable_set(:@_dependencies, [])
|
110
|
+
end
|
111
|
+
|
112
|
+
def append_features(base)
|
113
|
+
if base.instance_variable_defined?(:@_dependencies)
|
114
|
+
base.instance_variable_get(:@_dependencies) << self
|
115
|
+
false
|
116
|
+
else
|
117
|
+
return false if base < self
|
118
|
+
@_dependencies.each { |dep| base.include(dep) }
|
119
|
+
super
|
120
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
121
|
+
# if instance_variable_defined?(:@_included_block)
|
122
|
+
# base.class_eval(&@_included_block)
|
123
|
+
# end
|
124
|
+
if @_included_block
|
125
|
+
base.class_eval(&@_included_block)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def included(base = nil, &block)
|
131
|
+
if base.nil?
|
132
|
+
if instance_variable_defined?(:@_included_block)
|
133
|
+
raise MultipleIncludedBlocks
|
134
|
+
end
|
135
|
+
|
136
|
+
@_included_block = block
|
137
|
+
else
|
138
|
+
super
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def class_methods(&class_methods_module_definition)
|
143
|
+
mod = if const_defined?(:ClassMethods, false)
|
144
|
+
const_get(:ClassMethods)
|
145
|
+
else
|
146
|
+
const_set(:ClassMethods, Module.new)
|
147
|
+
end
|
148
|
+
|
149
|
+
mod.module_eval(&class_methods_module_definition)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -1,45 +1,8 @@
|
|
1
|
+
require 'active_support/core_ext/string/filters'
|
1
2
|
require 'active_support/core_ext/string/inflections'
|
2
3
|
|
3
4
|
class String
|
4
5
|
def parameterize
|
5
6
|
self.downcase.strip.gsub(/\W+/, '-')
|
6
7
|
end
|
7
|
-
|
8
|
-
def dasherize
|
9
|
-
result = `#{self}.replace(/[-_\s]+/g, '-')
|
10
|
-
.replace(/([A-Z\d]+)([A-Z][a-z])/g, '$1-$2')
|
11
|
-
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
12
|
-
.toLowerCase()`
|
13
|
-
result
|
14
|
-
end
|
15
|
-
|
16
|
-
def demodulize
|
17
|
-
%x{
|
18
|
-
var idx = #{self}.lastIndexOf('::');
|
19
|
-
|
20
|
-
if (idx > -1) {
|
21
|
-
return #{self}.substr(idx + 2);
|
22
|
-
}
|
23
|
-
|
24
|
-
return #{self};
|
25
|
-
}
|
26
|
-
end
|
27
|
-
|
28
|
-
def underscore
|
29
|
-
result = `#{self}.replace(/[-\s]+/g, '_')
|
30
|
-
.replace(/([A-Z\d]+)([A-Z][a-z])/g, '$1_$2')
|
31
|
-
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
32
|
-
.replace(/-/g, '_')
|
33
|
-
.toLowerCase()`
|
34
|
-
result
|
35
|
-
end
|
36
|
-
|
37
|
-
def camelize(first_letter = :upper)
|
38
|
-
result = `#{underscore}.replace(/(^|_)([^_]+)/g, function(match, pre, word, index) {
|
39
|
-
var capitalize = #{first_letter} === #{:upper} || index > 0;
|
40
|
-
return capitalize ? word.substr(0,1).toUpperCase()+word.substr(1) : word;
|
41
|
-
})`
|
42
|
-
result
|
43
|
-
end
|
44
|
-
alias_method :camelcase, :camelize
|
45
8
|
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class String
|
4
|
+
# Returns the string, first removing all whitespace on both ends of
|
5
|
+
# the string, and then changing remaining consecutive whitespace
|
6
|
+
# groups into one space each.
|
7
|
+
#
|
8
|
+
# Note that it handles both ASCII and Unicode whitespace.
|
9
|
+
#
|
10
|
+
# %{ Multi-line
|
11
|
+
# string }.squish # => "Multi-line string"
|
12
|
+
# " foo bar \n \t boo".squish # => "foo bar boo"
|
13
|
+
# def squish
|
14
|
+
# dup.squish!
|
15
|
+
# end
|
16
|
+
|
17
|
+
# Performs a destructive squish. See String#squish.
|
18
|
+
# str = " foo bar \n \t boo"
|
19
|
+
# str.squish! # => "foo bar boo"
|
20
|
+
# str # => "foo bar boo"
|
21
|
+
# def squish!
|
22
|
+
# gsub!(/[[:space:]]+/, " ")
|
23
|
+
# strip!
|
24
|
+
# self
|
25
|
+
# end
|
26
|
+
|
27
|
+
# Returns a new string with all occurrences of the patterns removed.
|
28
|
+
# str = "foo bar test"
|
29
|
+
# str.remove(" test") # => "foo bar"
|
30
|
+
# str.remove(" test", /bar/) # => "foo "
|
31
|
+
# str # => "foo bar test"
|
32
|
+
# def remove(*patterns)
|
33
|
+
# dup.remove!(*patterns)
|
34
|
+
# end
|
35
|
+
|
36
|
+
# Alters the string by removing all occurrences of the patterns.
|
37
|
+
# str = "foo bar test"
|
38
|
+
# str.remove!(" test", /bar/) # => "foo "
|
39
|
+
# str # => "foo "
|
40
|
+
# def remove!(*patterns)
|
41
|
+
# patterns.each do |pattern|
|
42
|
+
# gsub! pattern, ""
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# self
|
46
|
+
# end
|
47
|
+
|
48
|
+
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
|
49
|
+
#
|
50
|
+
# 'Once upon a time in a world far far away'.truncate(27)
|
51
|
+
# # => "Once upon a time in a wo..."
|
52
|
+
#
|
53
|
+
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
|
54
|
+
#
|
55
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
|
56
|
+
# # => "Once upon a time in a..."
|
57
|
+
#
|
58
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
|
59
|
+
# # => "Once upon a time in a..."
|
60
|
+
#
|
61
|
+
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
|
62
|
+
# for a total length not exceeding <tt>length</tt>:
|
63
|
+
#
|
64
|
+
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
|
65
|
+
# # => "And they f... (continued)"
|
66
|
+
def truncate(truncate_at, options = {})
|
67
|
+
return dup unless length > truncate_at
|
68
|
+
|
69
|
+
omission = options[:omission] || "..."
|
70
|
+
length_with_room_for_omission = truncate_at - omission.length
|
71
|
+
stop = \
|
72
|
+
if options[:separator]
|
73
|
+
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
|
74
|
+
else
|
75
|
+
length_with_room_for_omission
|
76
|
+
end
|
77
|
+
|
78
|
+
"#{self[0, stop]}#{omission}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
|
82
|
+
# breaking string encoding by splitting multibyte characters or breaking
|
83
|
+
# grapheme clusters ("perceptual characters") by truncating at combining
|
84
|
+
# characters.
|
85
|
+
#
|
86
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
|
87
|
+
# => 20
|
88
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
|
89
|
+
# => 80
|
90
|
+
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
|
91
|
+
# => "🔪🔪🔪🔪…"
|
92
|
+
#
|
93
|
+
# The truncated text ends with the <tt>:omission</tt> string, defaulting
|
94
|
+
# to "…", for a total length not exceeding <tt>bytesize</tt>.
|
95
|
+
# def truncate_bytes(truncate_at, omission: "…")
|
96
|
+
# omission ||= ""
|
97
|
+
#
|
98
|
+
# case
|
99
|
+
# when bytesize <= truncate_at
|
100
|
+
# dup
|
101
|
+
# when omission.bytesize > truncate_at
|
102
|
+
# raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
|
103
|
+
# when omission.bytesize == truncate_at
|
104
|
+
# omission.dup
|
105
|
+
# else
|
106
|
+
# self.class.new.tap do |cut|
|
107
|
+
# cut_at = truncate_at - omission.bytesize
|
108
|
+
#
|
109
|
+
# scan(/\X/) do |grapheme|
|
110
|
+
# if cut.bytesize + grapheme.bytesize <= cut_at
|
111
|
+
# cut << grapheme
|
112
|
+
# else
|
113
|
+
# break
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# cut << omission
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
# end
|
121
|
+
|
122
|
+
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
|
123
|
+
#
|
124
|
+
# 'Once upon a time in a world far far away'.truncate_words(4)
|
125
|
+
# # => "Once upon a time..."
|
126
|
+
#
|
127
|
+
# Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
|
128
|
+
#
|
129
|
+
# 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
|
130
|
+
# # => "Once<br>upon<br>a<br>time<br>in..."
|
131
|
+
#
|
132
|
+
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
|
133
|
+
#
|
134
|
+
# 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
|
135
|
+
# # => "And they found that many... (continued)"
|
136
|
+
# def truncate_words(words_count, options = {})
|
137
|
+
# sep = options[:separator] || /\s+/
|
138
|
+
# sep = Regexp.escape(sep.to_s) unless Regexp === sep
|
139
|
+
# if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
|
140
|
+
# $1 + (options[:omission] || "...")
|
141
|
+
# else
|
142
|
+
# dup
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
end
|
146
|
+
|