joys 0.1.1 → 0.1.3
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 +13 -0
- data/README.md +335 -160
- data/joys-0.1.0.gem +0 -0
- data/joys-0.1.1.gem +0 -0
- data/joys-0.1.2.gem +0 -0
- data/lib/.DS_Store +0 -0
- data/lib/joys/cli.rb +1554 -0
- data/lib/joys/config.rb +133 -0
- data/lib/joys/core.rb +189 -0
- data/lib/joys/data.rb +477 -0
- data/lib/joys/helpers.rb +138 -0
- data/lib/joys/ssg.rb +597 -0
- data/lib/joys/styles.rb +156 -0
- data/lib/joys/tags.rb +124 -0
- data/lib/joys/toys.rb +328 -0
- data/lib/joys/version.rb +1 -1
- data/lib/joys.rb +5 -5
- metadata +13 -2
- data/.DS_Store +0 -0
data/lib/joys/styles.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# lib/joys/styles.rb - Simplified Component Styling
|
3
|
+
require 'fileutils'
|
4
|
+
require 'set'
|
5
|
+
module Joys
|
6
|
+
module Styles
|
7
|
+
def self.compile_component_styles(comp_name, base_css, media_queries, scoped)
|
8
|
+
scope_prefix = scoped ? ".#{comp_name.tr('_', '-')} " : ""
|
9
|
+
processed_base_css = base_css.map { |css| scope_css(css, scope_prefix) }
|
10
|
+
processed_media_queries = {}
|
11
|
+
|
12
|
+
# FIX: Actually iterate over the media_queries parameter
|
13
|
+
media_queries.each do |key, css_rules|
|
14
|
+
scoped_rules = css_rules.map { |css| scope_css(css, scope_prefix) }
|
15
|
+
processed_media_queries[key] = scoped_rules
|
16
|
+
end
|
17
|
+
|
18
|
+
Joys.compiled_styles[comp_name] = {
|
19
|
+
base_css: processed_base_css,
|
20
|
+
media_queries: processed_media_queries,
|
21
|
+
container_queries: {} # Keep this for compatibility, but media_queries contains all now
|
22
|
+
}.freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def self.render_consolidated_styles(used_components)
|
28
|
+
return "" if used_components.empty?
|
29
|
+
cache_key = used_components.to_a.sort.join(',')
|
30
|
+
return Joys.consolidated_cache[cache_key] if Joys.consolidated_cache.key?(cache_key)
|
31
|
+
buffer = String.new(capacity: 4096)
|
32
|
+
min_queries = Hash.new { |h,k| h[k] = [] }
|
33
|
+
max_queries = Hash.new { |h,k| h[k] = [] }
|
34
|
+
minmax_queries = []
|
35
|
+
container_min_queries = Hash.new { |h,k| h[k] = [] }
|
36
|
+
container_max_queries = Hash.new { |h,k| h[k] = [] }
|
37
|
+
container_minmax_queries = []
|
38
|
+
container_named_queries = Hash.new { |h,k| h[k] = {min: Hash.new { |h,k| h[k] = [] }, max: Hash.new { |h,k| h[k] = [] }, minmax: []} }
|
39
|
+
seen_base = Set.new
|
40
|
+
|
41
|
+
used_components.each do |comp|
|
42
|
+
styles = Joys.compiled_styles[comp] || next
|
43
|
+
styles[:base_css].each do |rule|
|
44
|
+
next if seen_base.include?(rule)
|
45
|
+
buffer << rule
|
46
|
+
seen_base << rule
|
47
|
+
end
|
48
|
+
styles[:media_queries].each do |key, rules|
|
49
|
+
case key
|
50
|
+
# FIX: Updated regex patterns to match the actual key formats
|
51
|
+
when /^m-min-(\d+)$/
|
52
|
+
min_queries[$1.to_i].concat(rules)
|
53
|
+
when /^m-max-(\d+)$/
|
54
|
+
max_queries[$1.to_i].concat(rules)
|
55
|
+
when /^m-minmax-(\d+)-(\d+)$/
|
56
|
+
minmax_queries << { min: $1.to_i, max: $2.to_i, css: rules }
|
57
|
+
when /^c-min-(\d+)$/
|
58
|
+
container_min_queries[$1.to_i].concat(rules)
|
59
|
+
when /^c-max-(\d+)$/
|
60
|
+
container_max_queries[$1.to_i].concat(rules)
|
61
|
+
when /^c-minmax-(\d+)-(\d+)$/
|
62
|
+
container_minmax_queries << { min: $1.to_i, max: $2.to_i, css: rules }
|
63
|
+
when /^c-([^-]+)-min-(\d+)$/
|
64
|
+
name, size = $1, $2.to_i
|
65
|
+
container_named_queries[name][:min][size].concat(rules)
|
66
|
+
when /^c-([^-]+)-max-(\d+)$/
|
67
|
+
name, size = $1, $2.to_i
|
68
|
+
container_named_queries[name][:max][size].concat(rules)
|
69
|
+
when /^c-([^-]+)-minmax-(\d+)-(\d+)$/
|
70
|
+
name, min_size, max_size = $1, $2.to_i, $3.to_i
|
71
|
+
container_named_queries[name][:minmax] << { min: min_size, max: max_size, css: rules }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
max_queries.sort.reverse.each do |bp, rules|
|
77
|
+
buffer << "@media (max-width: #{bp}px){"
|
78
|
+
rules.uniq.each { |r| buffer << r }
|
79
|
+
buffer << "}"
|
80
|
+
end
|
81
|
+
min_queries.sort.each do |bp, rules|
|
82
|
+
buffer << "@media (min-width: #{bp}px){"
|
83
|
+
rules.uniq.each { |r| buffer << r }
|
84
|
+
buffer << "}"
|
85
|
+
end
|
86
|
+
minmax_queries.sort_by { |q| [q[:min], q[:max]] }.each do |q|
|
87
|
+
buffer << "@media (min-width: #{q[:min]}px) and (max-width: #{q[:max]}px){"
|
88
|
+
q[:css].uniq.each { |r| buffer << r }
|
89
|
+
buffer << "}"
|
90
|
+
end
|
91
|
+
container_max_queries.sort.reverse.each do |size, rules|
|
92
|
+
buffer << "@container (max-width: #{size}px){"
|
93
|
+
rules.uniq.each { |r| buffer << r }
|
94
|
+
buffer << "}"
|
95
|
+
end
|
96
|
+
container_min_queries.sort.each do |size, rules|
|
97
|
+
buffer << "@container (min-width: #{size}px){"
|
98
|
+
rules.uniq.each { |r| buffer << r }
|
99
|
+
buffer << "}"
|
100
|
+
end
|
101
|
+
container_minmax_queries.sort_by { |q| [q[:min], q[:max]] }.each do |q|
|
102
|
+
buffer << "@container (min-width: #{q[:min]}px) and (max-width: #{q[:max]}px){"
|
103
|
+
q[:css].uniq.each { |r| buffer << r }
|
104
|
+
buffer << "}"
|
105
|
+
end
|
106
|
+
container_named_queries.each do |name, queries|
|
107
|
+
queries[:max].sort.reverse.each do |size, rules|
|
108
|
+
buffer << "@container #{name} (max-width: #{size}px){"
|
109
|
+
rules.uniq.each { |r| buffer << r }
|
110
|
+
buffer << "}"
|
111
|
+
end
|
112
|
+
queries[:min].sort.each do |size, rules|
|
113
|
+
buffer << "@container #{name} (min-width: #{size}px){"
|
114
|
+
rules.uniq.each { |r| buffer << r }
|
115
|
+
buffer << "}"
|
116
|
+
end
|
117
|
+
queries[:minmax].sort_by { |q| [q[:min], q[:max]] }.each do |q|
|
118
|
+
buffer << "@container #{name} (min-width: #{q[:min]}px) and (max-width: #{q[:max]}px){"
|
119
|
+
q[:css].uniq.each { |r| buffer << r }
|
120
|
+
buffer << "}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
buffer.freeze.tap { |css| Joys.consolidated_cache[cache_key] = css }
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.render_external_styles(used_components)
|
127
|
+
return "" if used_components.empty?
|
128
|
+
cache_key = used_components.to_a.sort.join(',')
|
129
|
+
css_filename = "#{cache_key}.css"
|
130
|
+
#css_filename = "#{Digest::SHA256.hexdigest(cache_key)[0..12]}.css"
|
131
|
+
css_path = Joys.css_path || "public/css"
|
132
|
+
full_path = File.join(css_path, css_filename)
|
133
|
+
FileUtils.mkdir_p(css_path)
|
134
|
+
unless File.exist?(full_path)
|
135
|
+
css_content = render_consolidated_styles(used_components)
|
136
|
+
File.write(full_path, css_content)
|
137
|
+
end
|
138
|
+
"<link rel=\"stylesheet\" href=\"/css/#{css_filename}\">"
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.scope_css(css, scope_prefix)
|
142
|
+
return css if scope_prefix.empty?
|
143
|
+
selectors = css.split(',')
|
144
|
+
scoped_selectors = selectors.map do |selector|
|
145
|
+
selector.strip!
|
146
|
+
if match = selector.match(/^\s*\.([a-zA-Z][\w-]*)/)
|
147
|
+
#if selector.match(/^\s*\.([a-zA-Z][\w-]*)/)
|
148
|
+
"#{scope_prefix}#{selector}"
|
149
|
+
else
|
150
|
+
selector
|
151
|
+
end
|
152
|
+
end
|
153
|
+
scoped_selectors.join(', ')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/joys/tags.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# lib/joys/tags.rb
|
2
|
+
require_relative "hugdown"
|
3
|
+
module Joys
|
4
|
+
module Tags
|
5
|
+
VOID_TAGS = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
6
|
+
TAGS = %w[
|
7
|
+
a abbr address article aside audio b bdi bdo blockquote body button canvas
|
8
|
+
caption cite code colgroup data datalist dd del details dfn dialog div dl dt
|
9
|
+
em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
|
10
|
+
html i iframe ins kbd label legend li main map mark menu meter nav noscript
|
11
|
+
object ol optgroup option output p picture pre progress q rp rt ruby s samp
|
12
|
+
script section select small span strong style sub summary sup table tbody td
|
13
|
+
template textarea tfoot th thead time title tr u ul var video
|
14
|
+
].freeze
|
15
|
+
BOOLEAN_ATTRS = %w[
|
16
|
+
disabled readonly read_only multiple checked selected
|
17
|
+
auto_focus autofocus no_validate novalidate form_novalidate formnovalidate
|
18
|
+
hidden required open reversed scoped seamless muted auto_play autoplay
|
19
|
+
controls loop default inert item_scope itemscope
|
20
|
+
].to_set.freeze
|
21
|
+
|
22
|
+
CLS = ' class="'.freeze
|
23
|
+
QUO = '"'.freeze
|
24
|
+
BC = '>'.freeze
|
25
|
+
BO = '<'.freeze
|
26
|
+
GTS = '</'.freeze
|
27
|
+
EQ = '="'.freeze
|
28
|
+
|
29
|
+
VOID_TAGS.each do |tag|
|
30
|
+
define_method(tag) do |cs: nil, **attrs|
|
31
|
+
class_string = case cs
|
32
|
+
when String, Symbol then cs.to_s
|
33
|
+
when Array then cs.compact.reject { |c| c == false }.map(&:to_s).join(" ")
|
34
|
+
else ""
|
35
|
+
end
|
36
|
+
|
37
|
+
@bf << BO << tag
|
38
|
+
@bf << CLS << class_string << QUO unless class_string.empty?
|
39
|
+
|
40
|
+
attrs.each do |k, v|
|
41
|
+
key = k.to_s
|
42
|
+
|
43
|
+
if v.is_a?(Hash) && (key == "data" || key.start_with?("aria"))
|
44
|
+
prefix = key.tr('_', '-')
|
45
|
+
v.each do |sk, sv|
|
46
|
+
next if sv.nil? || sv == false # Skip nil and boolean false
|
47
|
+
attr_name = "#{prefix}-#{sk.to_s.tr('_', '-')}"
|
48
|
+
@bf << " #{attr_name}#{EQ}#{CGI.escapeHTML(sv.to_s)}#{QUO}"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
next if v.nil?
|
52
|
+
|
53
|
+
if BOOLEAN_ATTRS.include?(key)
|
54
|
+
@bf << " #{key.tr('_', '')}" if v
|
55
|
+
elsif v == false
|
56
|
+
next
|
57
|
+
else
|
58
|
+
attr_name = key.tr('_', '-')
|
59
|
+
@bf << " #{attr_name}#{EQ}#{CGI.escapeHTML(v.to_s)}#{QUO}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
@bf << BC
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
TAGS.each do |tag|
|
70
|
+
define_method(tag) do |content = nil, cs: nil, raw: false, **attrs, &block|
|
71
|
+
class_string = case cs
|
72
|
+
when String, Symbol then cs.to_s
|
73
|
+
when Array then cs.compact.reject { |c| c == false }.map(&:to_s).join(" ")
|
74
|
+
else ""
|
75
|
+
end
|
76
|
+
|
77
|
+
@bf << BO << tag
|
78
|
+
@bf << CLS << class_string << QUO unless class_string.empty?
|
79
|
+
|
80
|
+
attrs.each do |k, v|
|
81
|
+
key = k.to_s
|
82
|
+
|
83
|
+
if v.is_a?(Hash) && (key == "data" || key.start_with?("aria"))
|
84
|
+
prefix = key.tr('_', '-')
|
85
|
+
v.each do |sk, sv|
|
86
|
+
next if sv.nil? || sv == false # Skip nil and boolean false
|
87
|
+
attr_name = "#{prefix}-#{sk.to_s.tr('_', '-')}"
|
88
|
+
@bf << " #{attr_name}#{EQ}#{CGI.escapeHTML(sv.to_s)}#{QUO}"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
next if v.nil?
|
92
|
+
|
93
|
+
if BOOLEAN_ATTRS.include?(key)
|
94
|
+
@bf << " #{key.tr('_', '')}" if v
|
95
|
+
elsif v == false
|
96
|
+
next
|
97
|
+
else
|
98
|
+
attr_name = key.tr('_', '-')
|
99
|
+
@bf << " #{attr_name}#{EQ}#{CGI.escapeHTML(v.to_s)}#{QUO}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
@bf << BC
|
105
|
+
|
106
|
+
if block
|
107
|
+
instance_eval(&block)
|
108
|
+
elsif content
|
109
|
+
@bf << (raw ? content.to_s : CGI.escapeHTML(content.to_s))
|
110
|
+
end
|
111
|
+
|
112
|
+
@bf << GTS << tag << BC
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
define_method("#{tag}!") do |content = nil, cs: nil, **attrs, &block|
|
116
|
+
send(tag, content, cs: cs, raw: true, **attrs, &block)
|
117
|
+
end
|
118
|
+
define_method("#{tag}?") do |content = nil, cs: nil, **attrs, &block|
|
119
|
+
parsed_content = Joys::Config.markup_parser.call(content.to_s)
|
120
|
+
send(tag, parsed_content, cs: cs, raw: true, **attrs, &block)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/joys/toys.rb
ADDED
@@ -0,0 +1,328 @@
|
|
1
|
+
# Define a cycle helper that keeps its state per-key
|
2
|
+
# frozen_string_literal: true
|
3
|
+
# lib/joys/toys.rb - Intelligent Core Extensions
|
4
|
+
module Joys
|
5
|
+
module Toys
|
6
|
+
module CoreExtensions
|
7
|
+
module Numeric
|
8
|
+
|
9
|
+
# 3127613.filesize(:kb) => "3.0 GB", 1536.filesize => "1.5 KB"
|
10
|
+
def filesize(unit = :bytes)
|
11
|
+
# Convert input to bytes first
|
12
|
+
bytes = case unit
|
13
|
+
when :bytes, :b then self
|
14
|
+
when :kb then self * 1024
|
15
|
+
when :mb then self * 1024 * 1024
|
16
|
+
when :gb then self * 1024 * 1024 * 1024
|
17
|
+
when :tb then self * 1024 * 1024 * 1024 * 1024
|
18
|
+
else self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Then convert bytes to human readable
|
22
|
+
return "#{bytes} B" if bytes < 1024
|
23
|
+
|
24
|
+
units = [
|
25
|
+
[1024**4, 'TB'],
|
26
|
+
[1024**3, 'GB'],
|
27
|
+
[1024**2, 'MB'],
|
28
|
+
[1024, 'KB']
|
29
|
+
]
|
30
|
+
units.each do |threshold, suffix|
|
31
|
+
if bytes >= threshold
|
32
|
+
value = bytes.to_f / threshold
|
33
|
+
formatted = value % 1 == 0 ? value.to_i.to_s : "%.1f" % value
|
34
|
+
return "#{formatted} #{suffix}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
"#{bytes} B"
|
39
|
+
end
|
40
|
+
# 21 => "21st", 102 => "102nd"
|
41
|
+
def ordinal
|
42
|
+
return self.to_s if self < 0
|
43
|
+
|
44
|
+
suffix = case self.to_i % 100
|
45
|
+
when 11, 12, 13 then 'th'
|
46
|
+
else
|
47
|
+
case self.to_i % 10
|
48
|
+
when 1 then 'st'
|
49
|
+
when 2 then 'nd'
|
50
|
+
when 3 then 'rd'
|
51
|
+
else 'th'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
"#{self}#{suffix}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# 1200 => "1.2K", 1000000 => "1M", 1500000000 => "1.5B"
|
58
|
+
def compact
|
59
|
+
return self.to_s if self.abs < 1000
|
60
|
+
|
61
|
+
units = [
|
62
|
+
[1_000_000_000_000, 'T'],
|
63
|
+
[1_000_000_000, 'B'],
|
64
|
+
[1_000_000, 'M'],
|
65
|
+
[1_000, 'K']
|
66
|
+
]
|
67
|
+
|
68
|
+
units.each do |threshold, suffix|
|
69
|
+
if self.abs >= threshold
|
70
|
+
value = self.to_f / threshold
|
71
|
+
formatted = value % 1 == 0 ? value.to_i.to_s : "%.1f" % value
|
72
|
+
return "#{formatted}#{suffix}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
self.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
# 42 => "forty-two", 123 => "one hundred twenty-three"
|
80
|
+
def spell
|
81
|
+
return 'zero' if self == 0
|
82
|
+
return self.to_s if self < 0 || self >= 1_000_000
|
83
|
+
|
84
|
+
ones = %w[zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen]
|
85
|
+
tens = %w[zero ten twenty thirty forty fifty sixty seventy eighty ninety]
|
86
|
+
|
87
|
+
num = self.to_i
|
88
|
+
result = []
|
89
|
+
|
90
|
+
# Thousands
|
91
|
+
if num >= 1000
|
92
|
+
result << ones[num / 1000] + ' thousand'
|
93
|
+
num %= 1000
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hundreds
|
97
|
+
if num >= 100
|
98
|
+
result << ones[num / 100] + ' hundred'
|
99
|
+
num %= 100
|
100
|
+
end
|
101
|
+
|
102
|
+
# Tens and ones
|
103
|
+
if num >= 20
|
104
|
+
ten_part = tens[num / 10]
|
105
|
+
one_part = num % 10 > 0 ? ones[num % 10] : nil
|
106
|
+
result << [ten_part, one_part].compact.join('-')
|
107
|
+
elsif num > 0
|
108
|
+
result << ones[num]
|
109
|
+
end
|
110
|
+
|
111
|
+
result.join(' ')
|
112
|
+
end
|
113
|
+
|
114
|
+
# 4 => "IV", 1994 => "MCMXCIV"
|
115
|
+
def roman
|
116
|
+
return '' if self <= 0 || self >= 4000
|
117
|
+
|
118
|
+
values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
|
119
|
+
numerals = %w[M CM D CD C XC L XL X IX V IV I]
|
120
|
+
|
121
|
+
result = ''
|
122
|
+
num = self.to_i
|
123
|
+
|
124
|
+
values.each_with_index do |value, index|
|
125
|
+
count = num / value
|
126
|
+
result += numerals[index] * count
|
127
|
+
num -= value * count
|
128
|
+
end
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
# 10.20.cash('€') => "10.20€", 10.20.cash('$') => "$10.20"
|
134
|
+
def cash(currency = '$')
|
135
|
+
formatted = "%.2f" % self
|
136
|
+
|
137
|
+
# Currency positioning logic
|
138
|
+
case currency.to_s
|
139
|
+
when '$', '¥', '₩', '₹', '₪' # Prefix currencies
|
140
|
+
"#{currency}#{formatted}"
|
141
|
+
when '€', '£', '₽', 'kr', 'zł' # Suffix currencies
|
142
|
+
"#{formatted}#{currency}"
|
143
|
+
else # Unknown - default to prefix
|
144
|
+
"#{currency}#{formatted}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
module String
|
150
|
+
# "hello world-foo" => "Hello World Foo" (smart about separators)
|
151
|
+
def title
|
152
|
+
self.split(/[\s\-_]+/).map(&:capitalize).join(' ')
|
153
|
+
end
|
154
|
+
|
155
|
+
# "John Wesley Doe" => "JWD"
|
156
|
+
def initials
|
157
|
+
self.split(/\s+/).map { |word| word[0]&.upcase }.compact.join
|
158
|
+
end
|
159
|
+
|
160
|
+
# "hello_world" => "HelloWorld"
|
161
|
+
def camel
|
162
|
+
self.split(/[\s\-_]+/).map.with_index { |word, i|
|
163
|
+
i == 0 ? word.downcase : word.capitalize
|
164
|
+
}.join
|
165
|
+
end
|
166
|
+
|
167
|
+
# "HelloWorld" => "hello_world"
|
168
|
+
def snake
|
169
|
+
self.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
|
170
|
+
end
|
171
|
+
|
172
|
+
# "Hello World!" => "hello-world"
|
173
|
+
def slug
|
174
|
+
self.downcase
|
175
|
+
.gsub(/[^\w\s\-]/, '') # Remove special chars except spaces/hyphens
|
176
|
+
.strip
|
177
|
+
.gsub(/[\s\-_]+/, '-') # Convert spaces/underscores to hyphens
|
178
|
+
.gsub(/\-+/, '-') # Collapse multiple hyphens
|
179
|
+
.gsub(/^\-|\-$/, '') # Remove leading/trailing hyphens
|
180
|
+
end
|
181
|
+
|
182
|
+
# "1234567890" => "(123) 456-7890"
|
183
|
+
def phone
|
184
|
+
digits = self.gsub(/\D/, '')
|
185
|
+
return self if digits.length != 10
|
186
|
+
|
187
|
+
"(#{digits[0..2]}) #{digits[3..5]}-#{digits[6..9]}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# "Long text here".snip(10) => "Long te..." (smart ellipsis)
|
191
|
+
def snip(length, ellipsis = '...')
|
192
|
+
return self if self.length <= length
|
193
|
+
return self if length < ellipsis.length + 1 # Don't make it longer!
|
194
|
+
|
195
|
+
# Smart truncation - try to break on word boundary near the limit
|
196
|
+
if length > 10
|
197
|
+
word_break = self[0..length-ellipsis.length].rindex(/\s/)
|
198
|
+
if word_break && word_break > length * 0.7 # Only if reasonably close
|
199
|
+
return self[0..word_break-1] + ellipsis
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
self[0..length-ellipsis.length-1] + ellipsis
|
204
|
+
end
|
205
|
+
|
206
|
+
# "hello world".hilite("world") => "hello <mark>world</mark>"
|
207
|
+
def hilite(term)
|
208
|
+
return self if term.nil? || term.empty?
|
209
|
+
self.gsub(/(#{Regexp.escape(term)})/i, '<mark>\1</mark>')
|
210
|
+
end
|
211
|
+
|
212
|
+
# "Secret info here".redact(/\w{6,}/) => "****** info ****"
|
213
|
+
def redact(pattern = /\w{4,}/)
|
214
|
+
self.gsub(pattern) { |match| '*' * match.length }
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
module Time
|
219
|
+
# 2.hours.ago.ago => "2h ago" (smart recent time)
|
220
|
+
def ago(format = :short)
|
221
|
+
diff = ::Time.now - self
|
222
|
+
return 'just now' if diff < 60
|
223
|
+
|
224
|
+
case format
|
225
|
+
when :short
|
226
|
+
case diff
|
227
|
+
when 0...3600 then "#{(diff/60).to_i}m ago"
|
228
|
+
when 3600...86400 then "#{(diff/3600).to_i}h ago"
|
229
|
+
when 86400...2592000 then "#{(diff/86400).to_i}d ago"
|
230
|
+
else strftime('%b %d')
|
231
|
+
end
|
232
|
+
when :long
|
233
|
+
case diff
|
234
|
+
when 0...3600 then "#{(diff/60).to_i} minutes ago"
|
235
|
+
when 3600...86400 then "#{(diff/3600).to_i} hours ago"
|
236
|
+
when 86400...2592000 then "#{(diff/86400).to_i} days ago"
|
237
|
+
else strftime('%B %d, %Y')
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
module Date
|
244
|
+
# Date.today.weekday => "Mon", Date.today.weekday(:long) => "Monday"
|
245
|
+
def weekday(format = :short)
|
246
|
+
format == :long ? strftime('%A') : strftime('%a')
|
247
|
+
end
|
248
|
+
|
249
|
+
# Date.new(2024,6,15).season => "Summer"
|
250
|
+
def season
|
251
|
+
month = self.month
|
252
|
+
case month
|
253
|
+
when 12, 1, 2 then 'Winter'
|
254
|
+
when 3, 4, 5 then 'Spring'
|
255
|
+
when 6, 7, 8 then 'Summer'
|
256
|
+
when 9, 10, 11 then 'Fall'
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Date.today.smart => "Today", Date.yesterday.smart => "Yesterday"
|
261
|
+
def smart
|
262
|
+
today = Date.today
|
263
|
+
return 'Today' if self == today
|
264
|
+
return 'Yesterday' if self == today - 1
|
265
|
+
return 'Tomorrow' if self == today + 1
|
266
|
+
|
267
|
+
# This year - show month/day
|
268
|
+
if self.year == today.year
|
269
|
+
strftime('%b %d')
|
270
|
+
else
|
271
|
+
strftime('%b %d, %Y')
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Date.today.schedule("+2d") => 2 days from now, Date.today.schedule("-1w") => 1 week ago
|
276
|
+
def schedule(offset_string)
|
277
|
+
match = offset_string.match(/([+-]?)(\d+)([dwmyh])/)
|
278
|
+
return self unless match
|
279
|
+
|
280
|
+
sign, amount, unit = match.captures
|
281
|
+
multiplier = sign == '-' ? -1 : 1
|
282
|
+
amount = amount.to_i * multiplier
|
283
|
+
|
284
|
+
case unit
|
285
|
+
when 'd' # days
|
286
|
+
self + amount
|
287
|
+
when 'w' # weeks
|
288
|
+
self + (amount * 7)
|
289
|
+
when 'm' # months
|
290
|
+
self >> amount # Use Ruby's month arithmetic
|
291
|
+
when 'y' # years
|
292
|
+
self >> (amount * 12)
|
293
|
+
when 'h' # hours (convert to datetime)
|
294
|
+
(self.to_time + (amount * 3600)).to_date
|
295
|
+
else
|
296
|
+
self
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
module Integer
|
302
|
+
# 7265.duration => "2h 1m", 90.duration => "1m 30s"
|
303
|
+
def duration(format = :short)
|
304
|
+
seconds = self
|
305
|
+
return '0s' if seconds == 0
|
306
|
+
|
307
|
+
hours = seconds / 3600
|
308
|
+
minutes = (seconds % 3600) / 60
|
309
|
+
secs = seconds % 60
|
310
|
+
|
311
|
+
parts = []
|
312
|
+
parts << "#{hours}h" if hours > 0
|
313
|
+
parts << "#{minutes}m" if minutes > 0
|
314
|
+
parts << "#{secs}s" if secs > 0 && hours == 0 # Only show seconds if under 1 hour
|
315
|
+
|
316
|
+
parts.join(' ')
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Apply the extensions
|
324
|
+
Numeric.include Joys::Toys::CoreExtensions::Numeric
|
325
|
+
String.include Joys::Toys::CoreExtensions::String
|
326
|
+
Time.include Joys::Toys::CoreExtensions::Time
|
327
|
+
Date.include Joys::Toys::CoreExtensions::Date
|
328
|
+
Integer.include Joys::Toys::CoreExtensions::Integer
|
data/lib/joys/version.rb
CHANGED
data/lib/joys.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "joys/version"
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
require_relative "joys/core"
|
5
|
+
require_relative "joys/helpers"
|
6
|
+
require_relative "joys/styles"
|
7
|
+
require_relative "joys/tags"
|
8
|
+
require_relative "joys/config"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: joys
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Garcia
|
@@ -18,15 +18,26 @@ executables: []
|
|
18
18
|
extensions: []
|
19
19
|
extra_rdoc_files: []
|
20
20
|
files:
|
21
|
-
- ".DS_Store"
|
22
21
|
- CHANGELOG.md
|
23
22
|
- CODE_OF_CONDUCT.md
|
24
23
|
- LICENSE
|
25
24
|
- LICENSE.txt
|
26
25
|
- README.md
|
27
26
|
- Rakefile
|
27
|
+
- joys-0.1.0.gem
|
28
|
+
- joys-0.1.1.gem
|
29
|
+
- joys-0.1.2.gem
|
28
30
|
- lib/.DS_Store
|
29
31
|
- lib/joys.rb
|
32
|
+
- lib/joys/cli.rb
|
33
|
+
- lib/joys/config.rb
|
34
|
+
- lib/joys/core.rb
|
35
|
+
- lib/joys/data.rb
|
36
|
+
- lib/joys/helpers.rb
|
37
|
+
- lib/joys/ssg.rb
|
38
|
+
- lib/joys/styles.rb
|
39
|
+
- lib/joys/tags.rb
|
40
|
+
- lib/joys/toys.rb
|
30
41
|
- lib/joys/version.rb
|
31
42
|
- sig/joys.rbs
|
32
43
|
homepage: https://github.com/activestylus/joys.git
|
data/.DS_Store
DELETED
Binary file
|