liquid_interpolatable.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. # :markup: markdown
  2. module LiquidInterpolatable
  3. extend ActiveSupport::Concern
  4. included do
  5. validate :validate_interpolation
  6. end
  7. def valid?(context = nil)
  8. super
  9. rescue Liquid::Error
  10. errors.empty?
  11. end
  12. def validate_interpolation
  13. interpolated
  14. rescue Liquid::Error => e
  15. errors.add(:options, "has an error with Liquid templating: #{e.message}")
  16. rescue
  17. # Calling `interpolated` without an incoming may naturally fail
  18. # with various errors when an agent expects one.
  19. end
  20. # Return the current interpolation context. Use this in your Agent
  21. # class to manipulate interpolation context for user.
  22. #
  23. # For example, to provide local variables:
  24. #
  25. # # Create a new scope to define variables in:
  26. # interpolation_context.stack {
  27. # interpolation_context['_something_'] = 42
  28. # # And user can say "{{_something_}}" in their options.
  29. # value = interpolated['some_key']
  30. # }
  31. #
  32. def interpolation_context
  33. @interpolation_context ||= Context.new(self)
  34. end
  35. # Take the given object as "self" in the current interpolation
  36. # context while running a given block.
  37. #
  38. # The most typical use case for this is to evaluate options for each
  39. # received event like this:
  40. #
  41. # def receive(incoming_events)
  42. # incoming_events.each do |event|
  43. # interpolate_with(event) do
  44. # # Handle each event based on "interpolated" options.
  45. # end
  46. # end
  47. # end
  48. def interpolate_with(self_object)
  49. case self_object
  50. when nil
  51. yield
  52. else
  53. context = interpolation_context
  54. begin
  55. context.environments.unshift(self_object.to_liquid)
  56. yield
  57. ensure
  58. context.environments.shift
  59. end
  60. end
  61. end
  62. def interpolate_options(options, self_object = nil)
  63. interpolate_with(self_object) do
  64. case options
  65. when String
  66. interpolate_string(options)
  67. when ActiveSupport::HashWithIndifferentAccess, Hash
  68. options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
  69. memo[key] = interpolate_options(value)
  70. }
  71. when Array
  72. options.map { |value| interpolate_options(value) }
  73. else
  74. options
  75. end
  76. end
  77. end
  78. def interpolated(self_object = nil)
  79. interpolate_with(self_object) do
  80. (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
  81. interpolate_options(options)
  82. end
  83. end
  84. def interpolate_string(string, self_object = nil)
  85. interpolate_with(self_object) do
  86. Liquid::Template.parse(string).render!(interpolation_context)
  87. end
  88. end
  89. class Context < Liquid::Context
  90. def initialize(agent)
  91. super({}, {}, { agent: agent }, true)
  92. end
  93. def hash
  94. [@environments, @scopes, @registers].hash
  95. end
  96. def eql?(other)
  97. other.environments == @environments &&
  98. other.scopes == @scopes &&
  99. other.registers == @registers
  100. end
  101. end
  102. require 'uri'
  103. module Filters
  104. # Percent encoding for URI conforming to RFC 3986.
  105. # Ref: http://tools.ietf.org/html/rfc3986#page-12
  106. def uri_escape(string)
  107. CGI.escape(string) rescue string
  108. end
  109. # Parse an input into a URI object, optionally resolving it
  110. # against a base URI if given.
  111. #
  112. # A URI object will have the following properties: scheme,
  113. # userinfo, host, port, registry, path, opaque, query, and
  114. # fragment.
  115. def to_uri(uri, base_uri = nil)
  116. if base_uri
  117. URI(base_uri) + uri.to_s
  118. else
  119. URI(uri.to_s)
  120. end
  121. rescue URI::Error
  122. nil
  123. end
  124. # Get the destination URL of a given URL by recursively following
  125. # redirects, up to 5 times in a row. If a given string is not a
  126. # valid absolute HTTP URL or in case of too many redirects, the
  127. # original string is returned. If any network/protocol error
  128. # occurs while following redirects, the last URL followed is
  129. # returned.
  130. def uri_expand(url, limit = 5)
  131. case url
  132. when URI
  133. uri = url
  134. else
  135. url = url.to_s
  136. begin
  137. uri = URI(url)
  138. rescue URI::Error
  139. return url
  140. end
  141. end
  142. http = Faraday.new do |builder|
  143. builder.adapter :net_http
  144. # builder.use FaradayMiddleware::FollowRedirects, limit: limit
  145. # ...does not handle non-HTTP URLs.
  146. end
  147. limit.times do
  148. begin
  149. case uri
  150. when URI::HTTP
  151. return uri.to_s unless uri.host
  152. response = http.head(uri)
  153. case response.status
  154. when 301, 302, 303, 307
  155. if location = response['location']
  156. uri += location
  157. next
  158. end
  159. end
  160. end
  161. rescue URI::Error, Faraday::Error, SystemCallError => e
  162. logger.error "#{e.class} in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]: #{e.message}:\n#{e.backtrace.join("\n")}"
  163. end
  164. return uri.to_s
  165. end
  166. logger.error "Too many rediretions in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]"
  167. url
  168. end
  169. # Unescape (basic) HTML entities in a string
  170. #
  171. # This currently decodes the following entities only: "&apos;",
  172. # "&quot;", "&lt;", "&gt;", "&amp;", "&#dd;" and "&#xhh;".
  173. def unescape(input)
  174. CGI.unescapeHTML(input) rescue input
  175. end
  176. # Escape a string for use in XPath expression
  177. def to_xpath(string)
  178. subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
  179. case x
  180. when /"/
  181. %Q{'#{x}'}
  182. else
  183. %Q{"#{x}"}
  184. end
  185. }
  186. if subs.size == 1
  187. subs.first
  188. else
  189. 'concat(' << subs.join(', ') << ')'
  190. end
  191. end
  192. def regex_replace(input, regex, replacement = nil)
  193. input.to_s.gsub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  194. end
  195. def regex_replace_first(input, regex, replacement = nil)
  196. input.to_s.sub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  197. end
  198. # Serializes data as JSON
  199. def json(input)
  200. JSON.dump(input)
  201. end
  202. private
  203. def logger
  204. @@logger ||=
  205. if defined?(Rails)
  206. Rails.logger
  207. else
  208. require 'logger'
  209. Logger.new(STDERR)
  210. end
  211. end
  212. BACKSLASH = "\\".freeze
  213. UNESCAPE = {
  214. "a" => "\a",
  215. "b" => "\b",
  216. "e" => "\e",
  217. "f" => "\f",
  218. "n" => "\n",
  219. "r" => "\r",
  220. "s" => " ",
  221. "t" => "\t",
  222. "v" => "\v",
  223. }
  224. # Unescape a replacement text for use in the second argument of
  225. # gsub/sub. The following escape sequences are recognized:
  226. #
  227. # - "\\" (backslash itself)
  228. # - "\a" (alert)
  229. # - "\b" (backspace)
  230. # - "\e" (escape)
  231. # - "\f" (form feed)
  232. # - "\n" (new line)
  233. # - "\r" (carriage return)
  234. # - "\s" (space)
  235. # - "\t" (horizontal tab)
  236. # - "\u{XXXX}" (unicode codepoint)
  237. # - "\v" (vertical tab)
  238. # - "\xXX" (hexadecimal character)
  239. # - "\1".."\9" (numbered capture groups)
  240. # - "\+" (last capture group)
  241. # - "\k<name>" (named capture group)
  242. # - "\&" or "\0" (complete matched text)
  243. # - "\`" (string before match)
  244. # - "\'" (string after match)
  245. #
  246. # Octal escape sequences are deliberately unsupported to avoid
  247. # conflict with numbered capture groups. Rather obscure Emacs
  248. # style character codes ("\C-x", "\M-\C-x" etc.) are also omitted
  249. # from this implementation.
  250. def unescape_replacement(s)
  251. s.gsub(/\\(?:([\d+&`'\\]|k<\w+>)|u\{([[:xdigit:]]+)\}|x([[:xdigit:]]{2})|(.))/) {
  252. if c = $1
  253. BACKSLASH + c
  254. elsif c = ($2 && [$2.to_i(16)].pack('U')) ||
  255. ($3 && [$3.to_i(16)].pack('C'))
  256. if c == BACKSLASH
  257. BACKSLASH + c
  258. else
  259. c
  260. end
  261. else
  262. UNESCAPE[$4] || $4
  263. end
  264. }
  265. end
  266. end
  267. Liquid::Template.register_filter(LiquidInterpolatable::Filters)
  268. module Tags
  269. class Credential < Liquid::Tag
  270. def initialize(tag_name, name, tokens)
  271. super
  272. @credential_name = name.strip
  273. end
  274. def render(context)
  275. context.registers[:agent].credential(@credential_name)
  276. end
  277. end
  278. class LineBreak < Liquid::Tag
  279. def render(context)
  280. "\n"
  281. end
  282. end
  283. end
  284. Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
  285. Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
  286. module Blocks
  287. # Replace every occurrence of a given regex pattern in the first
  288. # "in" block with the result of the "with" block in which the
  289. # variable `match` is set for each iteration, which can be used as
  290. # follows:
  291. #
  292. # - `match[0]` or just `match`: the whole matching string
  293. # - `match[1]`..`match[n]`: strings matching the numbered capture groups
  294. # - `match.size`: total number of the elements above (n+1)
  295. # - `match.names`: array of names of named capture groups
  296. # - `match[name]`..: strings matching the named capture groups
  297. # - `match.pre_match`: string preceding the match
  298. # - `match.post_match`: string following the match
  299. # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
  300. #
  301. # If named captures (`(?<name>...)`) are used in the pattern, they
  302. # are also made accessible as variables. Note that if numbered
  303. # captures are used mixed with named captures, you could get
  304. # unexpected results.
  305. #
  306. # Example usage:
  307. #
  308. # {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
  309. # {% assign fullname = "Doe, John A." %}
  310. # {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
  311. #
  312. # Use Me Like This.
  313. #
  314. # John A. Doe
  315. #
  316. class RegexReplace < Liquid::Block
  317. Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
  318. def initialize(tag_name, markup, tokens)
  319. super
  320. case markup
  321. when Syntax
  322. @regexp = $1
  323. else
  324. raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
  325. end
  326. @nodelist = @in_block = []
  327. @with_block = nil
  328. end
  329. def nodelist
  330. if @with_block
  331. @in_block + @with_block
  332. else
  333. @in_block
  334. end
  335. end
  336. def unknown_tag(tag, markup, tokens)
  337. return super unless tag == 'with'.freeze
  338. @nodelist = @with_block = []
  339. end
  340. def render(context)
  341. begin
  342. regexp = Regexp.new(context[@regexp].to_s)
  343. rescue ::SyntaxError => e
  344. raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
  345. end
  346. subject = render_all(@in_block, context)
  347. subject.send(first? ? :sub : :gsub, regexp) {
  348. next '' unless @with_block
  349. m = Regexp.last_match
  350. context.stack do
  351. m.names.each do |name|
  352. context[name] = m[name]
  353. end
  354. context['match'.freeze] = m
  355. render_all(@with_block, context)
  356. end
  357. }
  358. end
  359. def first?
  360. @tag_name.end_with?('_first'.freeze)
  361. end
  362. end
  363. end
  364. Liquid::Template.register_tag('regex_replace', LiquidInterpolatable::Blocks::RegexReplace)
  365. Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
  366. end