When creating a custom Jekyll tag, what is the `tokens` arg used for? - ruby

The Jekyll docs say we can create a custom tag:
module Jekyll
class TestTag < Liquid::Tag
def initialize(tag_name, text, tokens)
super
#text = text
#tokens = tokens
end
def render(context)
"text: #{#text} tokens: #{#tokens}"
end
end
end
Liquid::Template.register_tag('test', Jekyll::TestTag)
It seems like initialize is a built-in function, although the docs don't explicitly say that.
When I include this tag in a page:
{% test hallo world %}
I get:
text: hallo world tokens: {:locale=>#<Liquid::I18n:0x007fd62dbd5e38
#path=”/Library/Ruby/Gems/2.0.0/gems/liquid-3.0.6/lib/liquid/locales/en.yml”>,
:line_numbers=>true}
Where are these tokens coming from? What do they do? Can I set tokens myself?

Where are these tokens coming from?
You are using the super keyword, that means it calls the initialize method of its parent class, in this case Liquid::Tag, it is the constructor of the class and creates a new instance of Tag.
What do they do?
The tokens argument:
is a hash that stores Liquid options. By default it
has two keys: :locale and :line_numbers, the first is a Liquid::I18n
object, and the second, a boolean parameter that determines if error
messages should display the line number the error occurred. This
argument is used mostly to display localized error messages on Liquid
built-in Tags and Filters.

when defining a method using the super keyword tells the parser to look for a method of the same name along the lookup path.
Liquid::Tag has an initialize method, and that is where those tokens are most likely coming from.

Related

Document a method returning a lambda

I have an object with a method returning a lambda:
class Book
def mark_page(marker_color)
lambda do |page|
page.mark(marker_color)
end
end
end
And I want to document this Book#mark_page method using yardoc syntax. However, I cannot find anything about lambdas in this documentation.
Intuitively, I'd go for something like:
# #return [Proc(Page)]
Since yardoc.org/types is parsing it as:
a Proc containing (a Page)
PS: not so sure about the documentation tag. Feel free to remove it if not appropriate...

Calling one jekyll plugin from another

I'm writing a jekyll plugin to create a custom tag. It takes an argument and spits out a string of HTML. I've got it mostly working - I can pass it arguments and get back HTML based on those arguments. Great.
Here's what has me stumped: I want to include the render of another plugin as part of my own.
My aspirational plugin is jekyll_icon_list, the plugin I want to use is jekyll-inline-svg. Here's the (abbreviated) code:
require 'jekyll_icon_list/version'
require 'jekyll'
require 'jekyll-inline-svg'
module JekyllIconList
class IconList < Liquid::Tag
def initialize(tag_name, raw_args, tokens)
#raw_args = raw_args
#tokens = tokens
super
end
def parse_arguments(raw_args, settings)
# (Unrelated stuff)
end
def generate_image(icon, settings, context)
# (Unrelated stuff)
# Problem Here:
Liquid::Tag.parse(
'svg',
icon,
#tokens,
Liquid::ParseContext.new
).render(context)
end
def render(context)
# Builds my HTML, using generate_image in the process
end
end
end
Liquid::Template.register_tag('iconlist', JekyllIconList::IconList)
This doesn't throw any errors, but it also doesn't return anything at all.
Other things I've tried:
Jekyll::Tags::JekylInlineSvg.new(
returns a private method error. Jekyll doesn't want me making my own tags directly.
'{% svg #{icon} %}'
Returns exactly that literally with the icon substituted in; jekyll clearly doesn't parse the same file twice.
I'm trying to figure it out from Jekyll's source, but I'm not so practiced at reading source code and keep hitting dead ends. Can anyone point me in the right direction? Much appreciated.
Answering my own question:
def build_svg(icon_filename)
tag = "{% svg #{icon_filename} %}"
liquid_parse(tag)
end
def liquid_parse(input)
Liquid::Template.parse(input).render(#context)
end
Basically create a tiny template consisting of the tag you want to call, and hand it off to Liquid for parsing.
Below is the dirty way, which I used before I found the proper way:
Jekyll::Tags::JekyllInlineSvg.send(:new, 'svg', icon_filename, #tokens).render(context)
I found this question and answer, and while it's correct, I wanted to provide a full end-to-end example.
I wanted to wrap Jekyll Scholar's {% cite %} tags in my own content:
module Jekyll
class RenderTimeTag < Liquid::Tag
def initialize(tag_name, text, tokens)
super
#text = text
end
def build_cite(content, context)
tag = "{% cite #{content} %}"
return liquid_parse(tag, context)
end
def liquid_parse(input, context)
template = Liquid::Template.parse(input)
template.render(context)
end
def render(context)
citation = build_cite(#text, context)
# Yeah, I know this is bad HTML:
"<span tabindex=\"0\" class=\"citeblock\">#{citation}</span>"
end
end
end
Liquid::Template.register_tag('pretty_cite', Jekyll::RenderTimeTag)

How does the syntax MODULE::METHODNAME('string') work

I recently had cause to use the nokogiri gem to parse html but while i going through their documentation, i came across this ruby syntax that i hadn't seen before
html_doc = Nokogiri::HTML('<html><body><h1>Mr. Belvedere Fan Club</h1></body></html>')
xml_doc = Nokogiri::XML('<root><aliens><alien><name>Alf</name></alien></aliens></root>')
The part of interest for me is Nokogiri::HTML('...'). This looks very much like a method invocation but i know ruby method names cannot be in capital letters. So i looked through code files nokogiri gem and i came across the following definition
module Nokogiri
class << self
###
# Parse HTML. Convenience method for Nokogiri::HTML::Document.parse
def HTML thing, url = nil, encoding = nil, options = XML::ParseOptions::DEFAULT_HTML, &block
Nokogiri::HTML::Document.parse(thing, url, encoding, options, &block)
end
end
# more code
end
I tried reproducing the same code
module How
class << self
def DOESTHISWORK
puts "In How Method"
end
end
end
How::DOESTHISWORK
But it keeps coming back with the error "uninitialized constant How::DOESTHISWORK (NameError)". I know it has to do with the method name starting in capitals but i just haven't been able to figure out how it works in nokogiri.
The difference is in the Nokogiri example the method is being called with parentheses and a parameter value which identifies it as a method call. Your DOESTHISWORK method takes no parameters but can be called with empty parentheses e.g.
irb(main):028:0> How::DOESTHISWORK()
In How Method
=> nil
If you add a parameter to your method that can also serve to identify it as a method like so:
irb(main):036:0> How::DOESTHISWORK 'some param'
Starting method names with a lowercase letter is good practice but isn't enforced. Something that begins with a capital letter is assumed to be a constant and will be looked up as such, this is why the parentheses or parameter is needed to indicate a method is being referred to. Another example:
irb(main):051:0> def Example
irb(main):052:1> puts "An example!"
irb(main):053:1> end
=> nil
irb(main):054:0> Example
NameError: uninitialized constant Example
from (irb):54
from /Users/mike/.rbenv/versions/1.9.3-p194/bin/irb:12:in `<main>'
irb(main):055:0> Example()
An example!
=> nil
I also found this post to be very helpful
What are the restrictions for method names in Ruby?
It's good practice, while not mandatory, to start the method name with
a lower-case character, because names that start with capital letters
are constants in Ruby. It's still possible to use a constant name for
a method, but you won't be able to invoke it without parentheses,
because the interpeter will look-up for the name as a constant

How to set a method dynamically as other class method

Im new to Ruby, and im creating a cli app with Thor and some additional gems. My problem is that i take user input (from the console) and pass the data as a variable to a existing method (This method is from a gem)
My method
def search(searchtype, searchterm)
search = OtherClass.new
result = search.search.searchtype keyword: "#{searchterm}"
puts result
# search.search.searchtype is not a method in the gem im using.
end
The OtherClass gem has these search methods: users, repos
The users method
def users(*args)
arguments(args, :required => [:keyword])
get_request("/legacy/user/search/#{escape_uri(keyword)}", arguments.params)
end
The repos method
def repos(*args)
arguments(args, :required => [:keyword])
get_request("/legacy/repos/search/#{escape_uri(keyword)}", arguments.params)
end
So how can i pass in the user data to the method from the OtherClass? Heres something like what i would want to do. The SEARCHTERM would be dynamically passed to the search.search object as a method parameter.
def search(SEARCHTYPE, searchterm)
search = OtherClass.new
result = search.search.SEARCHTYPE keyword: "#{searchterm}"
puts result
end
The "#{searchterm}" works as expected, but i also want to pass in the method to the search.search object dynamically, this could probably be done with if's but im sure theres a better way, maybe the Ruby way to solve this problem.
Finally i would want to be able to use this little program like this (the serch method)
./search.rb search opensource linux
(where opensource could be users, or another type of search, and linux could be the search keyword for the searchtype)
If this is possible i would apprechiate any help!
Thnx!
If you'd like to call a method dynamically, use Object#send.
http://ruby-doc.org/core-1.9.3/Object.html#method-i-send
I would caution against sending a method that was obtained by user input though, for security reasons.

Minitest spec custom matcher

I have a line in my test:
page.has_reply?("my reply").must_equal true
and to make it more readable I want to use a custom matcher:
page.must_have_reply "my reply"
Based on the docs for https://github.com/zenspider/minitest-matchers I expect I need to write a matcher which looks something like this:
def have_reply(text)
subject.has_css?('.comment_body', :text => text)
end
MiniTest::Unit::TestCase.register_matcher :have_reply, :have_reply
The problem is that I can't see how to get a reference to the subject (i.e. the page object). The docs say "Note subject must be the first argument in assertion" but that doesn't really help.
There is a little example, you can create a class which should responds to set of methods matches?, failure_message_for_should, failure_message_for_should_not.
In matches? method you can get the reference to the subject.
class MyMatcher
def initialize(text)
#text = text
end
def matches? subject
subject =~ /^#{#text}.*/
end
def failure_message_for_should
"expected to start with #{#text}"
end
def failure_message_for_should_not
"expected not to start with #{#text}"
end
end
def start_with(text)
MyMatcher.new(text)
end
MiniTest::Unit::TestCase.register_matcher :start_with, :start_with
describe 'something' do
it 'must start with...' do
page = 'my reply'
page.must_start_with 'my reply'
page.must_start_with 'my '
end
end
There are many ways to get what you want here. The easiest way is to not mess with assertions, expectations, or matchers at all and just use an assert. So, assuming you already have the has_reply? method defined, you could just use this:
assert page.has_reply?("my reply")
But, that doesn't get you the must_have_reply syntax you are asking for. And I doubt you really have a has_reply? method. So, let's start.
Your asked "how to get a reference to the subject (i.e. the page object)". In this case the subject is the object that the must_have_reply method is defined on. So, you should use this instead of subject. But its not as straightforward as all that. Matchers add a level of indirection that we don't have with the usual Assertions (assert_equal, refute_equal) or Expectations (must_be_equal, wont_be_equal). If you want to write a Matcher you need to implement the Matcher API.
Fortunately for you you don't really have to implement the API. Since it seems you are already intending on relying on Cabybara's have_css matcher, we can simply use Capybara's HaveSelector class and let it implement the proper API. We just need to create our own Matchers module with a method that returns a HaveSelector object.
# Require Minitest Matchers to make this all work
require "minitest/matchers"
# Require Capybara's matchers so you can use them
require "capybara/rspec/matchers"
# Create your own matchers module
module YourApp
module Matchers
def have_reply text
# Return a properly configured HaveSelector instance
Capybara::RSpecMatchers::HaveSelector.new(:css, ".comment_body", :text => text)
end
# Register module using minitest-matcher syntax
def self.included base
instance_methods.each do |name|
base.register_matcher name, name
end
end
end
end
Then, in your minitest_helper.rb file, you can include your Matchers module so you can use it. (This code will include the matcher in all tests.)
class MiniTest::Rails::ActiveSupport::TestCase
# Include your module in the test case
include YourApp::Matchers
end
Minitest Matchers does all the hard lifting. You can now you can use your matcher as an assertion:
def test_using_an_assertion
visit root_path
assert_have_reply page, "my reply"
end
Or, you can use your matcher as an expectation:
it "is an expectation" do
visit root_path
page.must_have_reply "my reply"
end
And finally you can use it with a subject:
describe "with a subject" do
before { visit root_path }
subject { page }
it { must have_reply("my reply") }
must { have_reply "my reply" }
end
Important: For this to work, you must be using 'gem minitest-matchers', '>= 1.2.0' because register_matcher is not defined in earlier versions of that gem.

Resources