opal-activesupport 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/opal/opal-activesupport.svg?branch=master)](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
|
+
|