liquor 0.1.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -9
  5. data/Gemfile +7 -0
  6. data/Guardfile +11 -0
  7. data/MIT-LICENSE +6 -2
  8. data/README.md +4 -122
  9. data/Rakefile +20 -23
  10. data/doc/language-spec.html +768 -0
  11. data/doc/language-spec.md +698 -0
  12. data/lib/liquor.rb +39 -68
  13. data/lib/liquor/ast_tools.rb +28 -0
  14. data/lib/liquor/compiler.rb +110 -0
  15. data/lib/liquor/context.rb +76 -254
  16. data/lib/liquor/diagnostics.rb +151 -0
  17. data/lib/liquor/drop/drop.rb +168 -0
  18. data/lib/liquor/drop/drop_delegation.rb +24 -0
  19. data/lib/liquor/drop/drop_scope.rb +118 -0
  20. data/lib/liquor/drop/dropable.rb +17 -0
  21. data/lib/liquor/emitter.rb +313 -0
  22. data/lib/liquor/extensions/kaminari.rb +14 -0
  23. data/lib/liquor/extensions/pagination.rb +235 -0
  24. data/lib/liquor/extensions/rails.rb +97 -0
  25. data/lib/liquor/extensions/thinking_sphinx.rb +14 -0
  26. data/lib/liquor/extensions/tire.rb +30 -0
  27. data/lib/liquor/external.rb +79 -0
  28. data/lib/liquor/function.rb +94 -0
  29. data/lib/liquor/grammar/lexer.rb +1223 -0
  30. data/lib/liquor/grammar/lexer.rl +297 -0
  31. data/lib/liquor/grammar/parser.racc +288 -0
  32. data/lib/liquor/grammar/parser.rb +885 -0
  33. data/lib/liquor/library.rb +41 -0
  34. data/lib/liquor/manager.rb +146 -0
  35. data/lib/liquor/runtime.rb +167 -0
  36. data/lib/liquor/stdlib/builtin_functions.rb +315 -0
  37. data/lib/liquor/stdlib/builtin_tags.rb +228 -0
  38. data/lib/liquor/stdlib/html_truncater.rb +162 -0
  39. data/lib/liquor/stdlib/partial_tags.rb +76 -0
  40. data/lib/liquor/tag.rb +83 -14
  41. data/lib/liquor/version.rb +1 -1
  42. data/liquor.gemspec +29 -6
  43. data/spec/builtins_spec.rb +264 -0
  44. data/spec/compiler_spec.rb +136 -0
  45. data/spec/context_spec.rb +49 -0
  46. data/spec/drop_delegation_spec.rb +21 -0
  47. data/spec/drop_spec.rb +222 -0
  48. data/spec/errors_spec.rb +40 -0
  49. data/spec/external_spec.rb +207 -0
  50. data/spec/function_spec.rb +80 -0
  51. data/spec/lexer_spec.rb +173 -0
  52. data/spec/library_spec.rb +18 -0
  53. data/spec/manager_spec.rb +84 -0
  54. data/spec/parser_spec.rb +381 -0
  55. data/spec/partials_spec.rb +74 -0
  56. data/spec/runtime_spec.rb +97 -0
  57. data/spec/spec_helper.rb +94 -0
  58. data/spec/tag_spec.rb +7 -0
  59. metadata +216 -173
  60. data/AUTHORS +0 -2
  61. data/CHANGELOG +0 -48
  62. data/Gemfile.lock +0 -91
  63. data/History.txt +0 -44
  64. data/LICENSE +0 -23
  65. data/example/server/example_servlet.rb +0 -37
  66. data/example/server/liquid_servlet.rb +0 -28
  67. data/example/server/liquor_servlet.rb +0 -28
  68. data/example/server/server.rb +0 -12
  69. data/example/server/templates/index.liquid +0 -6
  70. data/example/server/templates/index.liquor +0 -6
  71. data/example/server/templates/products.liquid +0 -45
  72. data/example/server/templates/products.liquor +0 -45
  73. data/init.rb +0 -8
  74. data/lib/extras/liquid_view.rb +0 -51
  75. data/lib/extras/liquor_view.rb +0 -51
  76. data/lib/liquor/block.rb +0 -101
  77. data/lib/liquor/condition.rb +0 -120
  78. data/lib/liquor/document.rb +0 -17
  79. data/lib/liquor/drop.rb +0 -256
  80. data/lib/liquor/errors.rb +0 -11
  81. data/lib/liquor/extensions.rb +0 -72
  82. data/lib/liquor/file_system.rb +0 -62
  83. data/lib/liquor/htmltags.rb +0 -74
  84. data/lib/liquor/module_ex.rb +0 -60
  85. data/lib/liquor/standardfilters.rb +0 -315
  86. data/lib/liquor/strainer.rb +0 -58
  87. data/lib/liquor/tags/assign.rb +0 -33
  88. data/lib/liquor/tags/capture.rb +0 -35
  89. data/lib/liquor/tags/case.rb +0 -83
  90. data/lib/liquor/tags/comment.rb +0 -9
  91. data/lib/liquor/tags/content_for.rb +0 -54
  92. data/lib/liquor/tags/cycle.rb +0 -59
  93. data/lib/liquor/tags/for.rb +0 -136
  94. data/lib/liquor/tags/if.rb +0 -80
  95. data/lib/liquor/tags/ifchanged.rb +0 -20
  96. data/lib/liquor/tags/include.rb +0 -56
  97. data/lib/liquor/tags/unless.rb +0 -33
  98. data/lib/liquor/tags/yield.rb +0 -49
  99. data/lib/liquor/template.rb +0 -181
  100. data/lib/liquor/variable.rb +0 -52
  101. data/performance/shopify.rb +0 -92
  102. data/performance/shopify/comment_form.rb +0 -33
  103. data/performance/shopify/database.rb +0 -45
  104. data/performance/shopify/json_filter.rb +0 -7
  105. data/performance/shopify/liquid.rb +0 -18
  106. data/performance/shopify/liquor.rb +0 -18
  107. data/performance/shopify/money_filter.rb +0 -18
  108. data/performance/shopify/paginate.rb +0 -93
  109. data/performance/shopify/shop_filter.rb +0 -98
  110. data/performance/shopify/tag_filter.rb +0 -25
  111. data/performance/shopify/vision.database.yml +0 -945
  112. data/performance/shopify/weight_filter.rb +0 -11
  113. data/performance/tests/dropify/article.liquid +0 -74
  114. data/performance/tests/dropify/blog.liquid +0 -33
  115. data/performance/tests/dropify/cart.liquid +0 -66
  116. data/performance/tests/dropify/collection.liquid +0 -22
  117. data/performance/tests/dropify/index.liquid +0 -47
  118. data/performance/tests/dropify/page.liquid +0 -8
  119. data/performance/tests/dropify/product.liquid +0 -68
  120. data/performance/tests/dropify/theme.liquid +0 -105
  121. data/performance/tests/ripen/article.liquid +0 -74
  122. data/performance/tests/ripen/blog.liquid +0 -13
  123. data/performance/tests/ripen/cart.liquid +0 -54
  124. data/performance/tests/ripen/collection.liquid +0 -29
  125. data/performance/tests/ripen/index.liquid +0 -32
  126. data/performance/tests/ripen/page.liquid +0 -4
  127. data/performance/tests/ripen/product.liquid +0 -75
  128. data/performance/tests/ripen/theme.liquid +0 -85
  129. data/performance/tests/tribble/404.liquid +0 -56
  130. data/performance/tests/tribble/article.liquid +0 -98
  131. data/performance/tests/tribble/blog.liquid +0 -41
  132. data/performance/tests/tribble/cart.liquid +0 -134
  133. data/performance/tests/tribble/collection.liquid +0 -70
  134. data/performance/tests/tribble/index.liquid +0 -94
  135. data/performance/tests/tribble/page.liquid +0 -56
  136. data/performance/tests/tribble/product.liquid +0 -116
  137. data/performance/tests/tribble/search.liquid +0 -51
  138. data/performance/tests/tribble/theme.liquid +0 -90
  139. data/performance/tests/vogue/article.liquid +0 -66
  140. data/performance/tests/vogue/blog.liquid +0 -32
  141. data/performance/tests/vogue/cart.liquid +0 -58
  142. data/performance/tests/vogue/collection.liquid +0 -19
  143. data/performance/tests/vogue/index.liquid +0 -22
  144. data/performance/tests/vogue/page.liquid +0 -3
  145. data/performance/tests/vogue/product.liquid +0 -62
  146. data/performance/tests/vogue/theme.liquid +0 -122
  147. data/test/assign_test.rb +0 -11
  148. data/test/block_test.rb +0 -58
  149. data/test/capture_test.rb +0 -41
  150. data/test/condition_test.rb +0 -115
  151. data/test/content_for_test.rb +0 -15
  152. data/test/context_test.rb +0 -479
  153. data/test/drop_test.rb +0 -162
  154. data/test/error_handling_test.rb +0 -89
  155. data/test/extra/breakpoint.rb +0 -547
  156. data/test/extra/caller.rb +0 -80
  157. data/test/file_system_test.rb +0 -30
  158. data/test/filter_test.rb +0 -147
  159. data/test/helper.rb +0 -24
  160. data/test/html_tag_test.rb +0 -31
  161. data/test/if_else_test.rb +0 -139
  162. data/test/include_tag_test.rb +0 -129
  163. data/test/module_ex_test.rb +0 -89
  164. data/test/output_test.rb +0 -121
  165. data/test/parsing_quirks_test.rb +0 -54
  166. data/test/regexp_test.rb +0 -45
  167. data/test/security_test.rb +0 -41
  168. data/test/standard_filter_test.rb +0 -170
  169. data/test/standard_tag_test.rb +0 -405
  170. data/test/statements_test.rb +0 -137
  171. data/test/strainer_test.rb +0 -27
  172. data/test/template_test.rb +0 -82
  173. data/test/test_helper.rb +0 -28
  174. data/test/unless_else_test.rb +0 -27
  175. data/test/variable_test.rb +0 -173
  176. data/test/yield_test.rb +0 -24
@@ -0,0 +1,41 @@
1
+ module Liquor
2
+ module Library
3
+ def self.included(klass)
4
+ klass.instance_exec {
5
+ extend ModuleMethods
6
+
7
+ @functions = []
8
+ @tags = []
9
+ }
10
+ end
11
+
12
+ module ModuleMethods
13
+ def export(compiler)
14
+ @functions.each do |function|
15
+ compiler.register_function function
16
+ end
17
+
18
+ @tags.each do |tag|
19
+ compiler.register_tag tag
20
+ end
21
+ end
22
+
23
+ def function(name, options={}, &block)
24
+ @functions << Function.new(name, options, &block)
25
+ end
26
+
27
+ def function_alias(name, other)
28
+ function = @functions.find { |f| f.name == other }
29
+ if function.nil?
30
+ raise "Cannot alias to function `#{other}', as it does not exist"
31
+ end
32
+
33
+ @functions << function.alias(name)
34
+ end
35
+
36
+ def tag(name, options={}, &block)
37
+ @tags << Tag.new(name, options, &block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,146 @@
1
+ module Liquor
2
+ class Manager
3
+ attr_reader :diagnostics
4
+
5
+ def initialize(options={})
6
+ import = options.delete(:import).to_a || []
7
+ @dump_ir = options.delete(:dump_intermediates)
8
+
9
+ if options.any?
10
+ raise "Unknown function options: #{options.keys.join ", "}"
11
+ end
12
+
13
+ @compiler = Liquor::Compiler.new(manager: self)
14
+ import.each do |library|
15
+ library.export @compiler
16
+ end
17
+
18
+ @parser = Liquor::Parser.new(@compiler.tags)
19
+
20
+ @sources = {}
21
+ @templates = {}
22
+ @partials = {}
23
+ @compiled_templates = nil
24
+
25
+ @diagnostics = []
26
+ end
27
+
28
+ def partial?(name)
29
+ name.start_with? '_'
30
+ end
31
+
32
+ def register_template(name, source, externals=[])
33
+ if partial? name
34
+ raise ::ArgumentError, "Template `#{name}' is a partial"
35
+ end
36
+
37
+ @sources[name] = source.dup
38
+
39
+ if @parser.parse(source, name)
40
+ @templates[name.to_s] = [ @parser.ast, externals ]
41
+ else
42
+ @diagnostics.concat @parser.errors
43
+ end
44
+ end
45
+
46
+ def register_layout(name, source, externals=[])
47
+ register_template name, source, externals + [ :_inner_template ]
48
+ end
49
+
50
+ def register_partial(name, source)
51
+ unless partial? name
52
+ raise ::ArgumentError, "Template `#{name}' is not a partial"
53
+ end
54
+
55
+ @sources[name] = source.dup
56
+
57
+ if @parser.parse(source, name)
58
+ @partials[name.to_s] = @parser.ast
59
+ else
60
+ @partials[name.to_s] = :syntax_error
61
+ @diagnostics.concat @parser.errors
62
+ end
63
+ end
64
+
65
+ def fetch_partial(name)
66
+ @partials[name.to_s]
67
+ end
68
+
69
+ def compile
70
+ @compiled_templates = {}
71
+
72
+ @templates.each do |name, (template, externals)|
73
+ @compiler.compile(template, externals, name)
74
+ dump_intermediates(name, template)
75
+
76
+ if @compiler.success?
77
+ @compiled_templates[name] = @compiler.code
78
+ else
79
+ @diagnostics.concat @compiler.diagnostics
80
+ end
81
+ end
82
+
83
+ success?
84
+ end
85
+
86
+ def errors
87
+ diagnostics.select &:error?
88
+ end
89
+
90
+ def success?
91
+ errors.none?
92
+ end
93
+
94
+ def has_template?(name)
95
+ !@compiled_templates[name].nil?
96
+ end
97
+
98
+ def render(name, externals={}, storage={})
99
+ if @compiled_templates.nil?
100
+ raise ::RuntimeError, "Call #compile before #render"
101
+ end
102
+
103
+ template = @compiled_templates[name]
104
+ if template.nil?
105
+ raise Error.new("Template `#{name}' does not exist")
106
+ end
107
+
108
+ template.call(externals, storage)
109
+ end
110
+
111
+ def render_with_layout(layout_name, layout_externals,
112
+ template_name, template_externals,
113
+ storage={})
114
+ template_result = render(template_name, template_externals, storage)
115
+ layout_result = render(layout_name, layout_externals.
116
+ merge(_inner_template: template_result), storage)
117
+
118
+ layout_result
119
+ end
120
+
121
+ def decorate(error)
122
+ decorated = []
123
+
124
+ if error.is_a? Diagnostic
125
+ if error.location
126
+ if file = error.location[:file]
127
+ decorated = error.decorate @sources[file]
128
+ end
129
+ end
130
+ end
131
+
132
+ decorated
133
+ end
134
+
135
+ protected
136
+
137
+ def dump_intermediates(template_name, liquor_source)
138
+ if @dump_ir
139
+ path = File.join(@dump_ir, "#{template_name}.liquor.rb")
140
+ if @compiler.success?
141
+ File.write(path, @compiler.source)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,167 @@
1
+ module Liquor
2
+ module Runtime
3
+ class DummyExternal
4
+ def liquor_send(method, args, loc=nil)
5
+ nil
6
+ end
7
+
8
+ def source
9
+ nil
10
+ end
11
+ end
12
+
13
+ def self.type(value)
14
+ case value
15
+ when nil; :null
16
+ when true, false; :boolean
17
+ when Integer; :integer
18
+ when String; :string
19
+ when Array; :tuple
20
+ when External; :external
21
+ else; :invalid
22
+ end
23
+ end
24
+
25
+ def self.pretty_type(value)
26
+ case value
27
+ when nil; "Null"
28
+ when true, false; "Boolean"
29
+ when Integer; "Integer"
30
+ when String; "String"
31
+ when Array; "Tuple"
32
+ when External; indexable?(value) ? "IndexableExternal" : "External"
33
+ else "_Foreign<#{value.class}>"
34
+ end
35
+ end
36
+
37
+ def self.default_value_of(type)
38
+ case type
39
+ when :null; nil
40
+ when :boolean; false
41
+ when :integer; 0
42
+ when :string; ""
43
+ when :tuple; []
44
+ when :external; DummyExternal.new
45
+ end
46
+ end
47
+
48
+ @diagnostics = nil
49
+
50
+ def self.capture_diagnostics
51
+ old_diagnostics = @diagnostics
52
+ @diagnostics = []
53
+
54
+ yield
55
+
56
+ @diagnostics
57
+ ensure
58
+ @diagnostics = old_diagnostics
59
+ end
60
+
61
+ @fatal_deprecations = false
62
+
63
+ def self.with_fatal_deprecations
64
+ old_fatal_deprecations = @fatal_deprecations
65
+ @fatal_deprecations = true
66
+
67
+ yield
68
+ ensure
69
+ @fatal_deprecations = old_fatal_deprecations
70
+ end
71
+
72
+ def self.soft_error(klass=TypeError, message, expectation, loc)
73
+ error = klass.new(message, loc)
74
+
75
+ if @diagnostics.nil?
76
+ raise error
77
+ else
78
+ error.set_backtrace(caller(2))
79
+ @diagnostics << error
80
+
81
+ default_value_of expectation
82
+ end
83
+ end
84
+
85
+ def self.deprecation(message, loc)
86
+ error = Liquor::Deprecation.new(message, loc)
87
+ error.set_backtrace(caller(2))
88
+
89
+ if @fatal_deprecations
90
+ raise error
91
+ elsif @diagnostics
92
+ @diagnostics << error
93
+ end
94
+ end
95
+
96
+ def self.indexable?(value)
97
+ value.is_a?(Array) ||
98
+ value.is_a?(External) &&
99
+ value.class.liquor_exports &&
100
+ value.class.liquor_exports.include?(:[])
101
+ end
102
+
103
+ def self.add!(left, left_loc, right, right_loc)
104
+ if left.is_a? Integer
105
+ right = integer! right, right_loc
106
+ elsif left.is_a? String
107
+ right = string! right, right_loc
108
+ elsif indexable?(left)
109
+ left = left.to_a
110
+ right = tuple! right, right_loc
111
+ else
112
+ return soft_error("Integer, String, Tuple or indexable External value expected, #{pretty_type(left)} found", :null, left_loc)
113
+ end
114
+
115
+ left + right
116
+ end
117
+
118
+ def self.integer!(value, loc)
119
+ unless value.is_a? Integer
120
+ return soft_error("Integer value expected, #{pretty_type(value)} found", :integer, loc)
121
+ end
122
+
123
+ value
124
+ end
125
+
126
+ def self.string!(value, loc)
127
+ if value.is_a? String
128
+ value
129
+ elsif value.is_a? Integer
130
+ value.to_s
131
+ else
132
+ return soft_error("String value expected, #{pretty_type(value)} found", :string, loc)
133
+ end
134
+ end
135
+
136
+ # Proper indexable external API:
137
+ # .[](index) => element fetch
138
+ # .size => element count
139
+ # .to_a => converts to Array
140
+
141
+ def self.tuple!(value, loc)
142
+ unless indexable?(value)
143
+ return soft_error("Tuple or indexable External value expected, #{pretty_type(value)} found", :tuple, loc)
144
+ end
145
+
146
+ value
147
+ end
148
+
149
+ def self.external!(value, loc)
150
+ unless value.is_a? External
151
+ return soft_error("External value expected, #{pretty_type(value)} found", :external, loc)
152
+ end
153
+
154
+ value
155
+ end
156
+
157
+ def self.interp!(value, loc)
158
+ unless value.is_a?(String) ||
159
+ value.is_a?(Integer) ||
160
+ value.nil?
161
+ return soft_error("String or Null value expected, #{pretty_type(value)} found", :string, loc)
162
+ end
163
+
164
+ value.to_s
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,315 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require_relative 'html_truncater'
4
+
5
+ module Liquor
6
+ module Builtins
7
+ #
8
+ # Universal functions
9
+ #
10
+
11
+ function "is_empty", unnamed_arg: :any do |arg,|
12
+ arg.nil? || (arg.respond_to?(:size) && arg.size == 0)
13
+ end
14
+
15
+ function "size", unnamed_arg: [:string, :tuple] do |arg,|
16
+ arg.size
17
+ end
18
+
19
+ #
20
+ # Type conversion functions
21
+ #
22
+
23
+ function "strftime",
24
+ unnamed_arg: :string,
25
+ mandatory_named_args: { format: :string } do |arg, kw|
26
+ begin
27
+ time = Time.parse(arg)
28
+
29
+ if defined?(I18n)
30
+ I18n.localize(time, format: kw[:format])
31
+ else
32
+ time.strftime(kw[:format])
33
+ end
34
+
35
+ rescue Exception => e
36
+ arg
37
+ end
38
+ end
39
+
40
+ function "to_number", unnamed_arg: [:integer, :string] do |arg,|
41
+ if arg.is_a? Integer
42
+ arg
43
+ else
44
+ arg.to_i
45
+ end
46
+ end
47
+
48
+ #
49
+ # Integer functions
50
+ #
51
+
52
+ function "is_even", unnamed_arg: :integer do |arg,|
53
+ (arg % 2) == 0
54
+ end
55
+
56
+ function "is_odd", unnamed_arg: :integer do |arg,|
57
+ (arg % 2) == 1
58
+ end
59
+
60
+ #
61
+ # String functions
62
+ #
63
+
64
+ function "downcase", unnamed_arg: :string do |arg,|
65
+ arg.downcase
66
+ end
67
+
68
+ function "upcase", unnamed_arg: :string do |arg,|
69
+ arg.upcase
70
+ end
71
+
72
+ function "capitalize", unnamed_arg: :string do |arg,|
73
+ arg.capitalize
74
+ end
75
+
76
+ function "starts_with", unnamed_arg: :string,
77
+ mandatory_named_args: { pattern: :string } do |arg, kw|
78
+ arg.start_with?(kw[:pattern])
79
+ end
80
+
81
+ function "strip_newlines", unnamed_arg: :string do |arg,|
82
+ arg.gsub("\n", "")
83
+ end
84
+
85
+ function "join", unnamed_arg: :tuple,
86
+ optional_named_args: { with: :string } do |arg, kw|
87
+ arg.flatten.join(kw[:with] || ' ')
88
+ end
89
+
90
+ function "split", unnamed_arg: :string,
91
+ optional_named_args: { by: :string } do |arg, kw|
92
+ arg.split(kw[:by] || ' ')
93
+ end
94
+
95
+ function "replace",
96
+ unnamed_arg: :string,
97
+ mandatory_named_args: {
98
+ pattern: :string,
99
+ replacement: :string
100
+ } do |arg, kw|
101
+ arg.gsub kw[:pattern], kw[:replacement]
102
+ end
103
+
104
+ function "replace_first",
105
+ unnamed_arg: :string,
106
+ mandatory_named_args: {
107
+ pattern: :string,
108
+ replacement: :string
109
+ } do |arg, kw|
110
+ arg.sub kw[:pattern], kw[:replacement]
111
+ end
112
+
113
+ function "remove",
114
+ unnamed_arg: :string,
115
+ mandatory_named_args: {
116
+ pattern: :string,
117
+ } do |arg, kw|
118
+ arg.gsub kw[:pattern], ''
119
+ end
120
+
121
+ function "remove_first",
122
+ unnamed_arg: :string,
123
+ mandatory_named_args: {
124
+ pattern: :string,
125
+ } do |arg, kw|
126
+ arg.sub kw[:pattern], ''
127
+ end
128
+
129
+ function "newline_to_br", unnamed_arg: :string do |arg,|
130
+ arg.gsub(/\n/, "<br>\n")
131
+ end
132
+
133
+ function "url_escape", unnamed_arg: :string do |arg,|
134
+ URI.encode_www_form_component(arg)
135
+ end
136
+
137
+ function "html_escape", unnamed_arg: [:string, :null] do |arg,|
138
+ if arg
139
+ if defined?(Rack::Utils)
140
+ Rack::Utils.escape_html(arg)
141
+ else
142
+ raise NotImplementedError, "html_escape() requires Rack"
143
+ end
144
+ end
145
+ end
146
+
147
+ function "html_escape_once", unnamed_arg: :string do |arg,|
148
+ if defined?(ActionView)
149
+ @html_escape_once_helper ||= Object.new.
150
+ tap { |obj| obj.extend ActionView::Helpers::TagHelper }
151
+ @html_escape_once_helper.escape_once(arg)
152
+ else
153
+ raise NotImplementedError, "html_escape_once() requires ActionView"
154
+ end
155
+ end
156
+ function_alias "h", "html_escape_once"
157
+
158
+ function "strip_html", unnamed_arg: :string do |arg,|
159
+ if defined?(HTML::Sanitizer)
160
+ HTML::FullSanitizer.new.sanitize(arg)
161
+ else
162
+ raise NotImplementedError, "escape_once() requires HTML::Sanitizer (try loading ActionView)"
163
+ end
164
+ end
165
+
166
+ function "decode_html_entities", unnamed_arg: :string do |arg,|
167
+ if defined?(HTMLEntities)
168
+ HTMLEntities.new.decode(arg)
169
+ else
170
+ raise NotImplementedError, "decode_html_entities() requires HTMLEntities"
171
+ end
172
+ end
173
+
174
+ #
175
+ # Tuple functions
176
+ #
177
+
178
+ function "compact", unnamed_arg: :tuple do |arg,|
179
+ arg.compact
180
+ end
181
+
182
+ function "reverse", unnamed_arg: :tuple do |arg,|
183
+ arg.reverse
184
+ end
185
+
186
+ function "uniq", unnamed_arg: :tuple do |arg, |
187
+ arg.uniq
188
+ end
189
+
190
+ minmax = lambda do |fn, fn_by, set, key|
191
+ if key
192
+ set.to_a.select { |r| !r.liquor_send(key, []).nil? }.
193
+ send(fn_by) { |r| r.liquor_send(key, []) }
194
+ else
195
+ set.to_a.send(fn)
196
+ end
197
+ end
198
+
199
+ function "min", unnamed_arg: [:tuple],
200
+ optional_named_args: {
201
+ by: :string
202
+ } do |source_set, kw|
203
+ minmax.call(:min, :min_by, source_set, kw[:by])
204
+ end
205
+
206
+ function "max", unnamed_arg: [:tuple],
207
+ optional_named_args: {
208
+ by: :string
209
+ } do |source_set, kw|
210
+ minmax.call(:max, :max_by, source_set, kw[:by])
211
+ end
212
+
213
+ function "in_groups_of",
214
+ unnamed_arg: [:tuple],
215
+ mandatory_named_args: { size: :integer },
216
+ optional_named_args: { fill_with: [:string, :boolean] } do |arg, kw|
217
+ if [].respond_to? :in_groups_of
218
+ arg.to_a.in_groups_of(kw[:size], kw[:fill_with])
219
+ else
220
+ raise NotImplementedError, "in_groups_of() requires ActiveSupport Array extensions"
221
+ end
222
+ end
223
+
224
+ function "in_groups",
225
+ unnamed_arg: [:tuple],
226
+ mandatory_named_args: { count: :integer },
227
+ optional_named_args: { fill_with: [:string, :boolean] } do |arg, kw|
228
+ if [].respond_to? :in_groups
229
+ arg.to_a.in_groups(kw[:count], kw[:fill_with])
230
+ else
231
+ raise NotImplementedError, "in_groups() requires ActiveSupport Array extensions"
232
+ end
233
+ end
234
+
235
+ # DEPRECATED
236
+ function "includes",
237
+ unnamed_arg: :tuple,
238
+ mandatory_named_args: { element: :any } do |arg, kw|
239
+ arg.include?(kw[:element])
240
+ end
241
+
242
+ #
243
+ # Intelligent truncation functions
244
+ #
245
+
246
+ function "truncate",
247
+ unnamed_arg: :string,
248
+ optional_named_args: {
249
+ length: :integer,
250
+ omission: :string
251
+ } do |arg, kw|
252
+ length = kw[:length] || 50
253
+ omission = kw[:omission] || '...'
254
+
255
+ truncate_at = length - kw[:omission].length
256
+ truncate_at = 0 if length < 0
257
+
258
+ if arg.length > length
259
+ arg[0...truncate_at] + omission
260
+ else
261
+ arg
262
+ end
263
+ end
264
+
265
+ function "truncate_words",
266
+ unnamed_arg: :string,
267
+ optional_named_args: {
268
+ words: :integer,
269
+ omission: :string
270
+ } do |arg, kw|
271
+ words = arg.split
272
+ length = kw[:words] || 15
273
+ omission = kw[:omission] || '...'
274
+
275
+ if words.length > length
276
+ words[0...length].join(" ") + omission
277
+ else
278
+ words.join(" ")
279
+ end
280
+ end
281
+
282
+ function "html_truncate",
283
+ unnamed_arg: :string,
284
+ optional_named_args: {
285
+ length: :integer,
286
+ omission: :string
287
+ } do |arg, kw|
288
+ length = kw[:length] || 50
289
+ omission = kw[:omission] || '...'
290
+
291
+ if defined?(Nokogiri)
292
+ HTMLTruncater.truncate(arg, length, omission)
293
+ else
294
+ raise NotImplementedError, "html_truncate() requires Nokogiri"
295
+ end
296
+ end
297
+
298
+ function "html_truncate_words",
299
+ unnamed_arg: :string,
300
+ optional_named_args: {
301
+ words: :integer,
302
+ omission: :string
303
+ } do |arg, kw|
304
+ length = kw[:words] || 15
305
+ omission = kw[:omission] || '...'
306
+
307
+ if defined?(Nokogiri)
308
+ HTMLTruncater.truncate_words(arg, length, omission)
309
+ else
310
+ raise NotImplementedError, "html_truncate_words() requires Nokogiri"
311
+ end
312
+ end
313
+
314
+ end
315
+ end