Add properties to a page from a Jekyll plugin - ruby

Say I want to have a page with content like this:
<h1>{{page.comment_count}} Comment(s)</h1>
{% for c in page.comment_list %}
<div>
<strong>{{c.title}}</strong><br/>
{{c.content}}
</div>
{% endfor %}
There are no variables on the page named comment_count or comment_list by default; instead I want these variables to be added to the page from a Jekyll plugin. Where is a safe place I can populate those fields from without interfering with Jekyll's existing code?
Or is there a better way of achieving a list of comments like this?

Unfortunately, there isn't presently the possibility to add these attributes without some messing with internal Jekyll stuff. We're on our way to adding hooks for #after_initialize, etc but aren't there yet.
My best suggestion is to add these attributes as I've done with my Octopress Date plugin on my blog. It uses Jekyll v1.2.0's Jekyll::Post#to_liquid method to add these attributes, which are collected via send(attr) on the Post:
class Jekyll::Post
def comment_count
comment_list.size
end
def comment_list
YAML.safe_load_file("_comments/#{self.id}.yml")
end
# Convert this post into a Hash for use in Liquid templates.
#
# Returns <Hash>
def to_liquid(attrs = ATTRIBUTES_FOR_LIQUID)
super(attrs + %w[
comment_count
comment_list
])
end
end
super(attrs + %w[ ... ]) will ensure that all the old attributes are still included, then collect the return values of the methods corresponding to the entries in the String array.
This is the best means of extending posts and pages so far.

Related

Overriding a context register in a Jekyll Block plugin

I'm trying to get more control over my post excerpts in Jekyll. Using the default excerpt code doesn't seem to work well in my case, as it (a) needs to be the first paragraph, and (b) is still rendered on the page. For most of my blog posts, I want either custom text, which isn't meant to be part of the actual blog post, or some text later on in the blog post. I was hoping to do this with a block, that can optionally take a parameter to render the content (although I'll add in that specific feature later). I've currently got this proof-of-concept plugin:
require "jekyll"
module Jekyll
class RenderExcerptBlock < Liquid::Block
def render(context)
page = context.registers[:page]
content = super
page["excerpt"] = content.lines[0..50]
content
end
end
end
Liquid::Template.register_tag "excerpt", Jekyll::RenderExcerptBlock
Which would theoretically let me do this:
{% excerpt %}
A snippet of this post
{% endexcerpt %}
However, this gives the error when running jekyll build:
Liquid Exception: Key excerpt cannot be set in the drop. in /Users/nick.chambers/blog/_posts/2020-01-21-unix-shell-scripting-with-bash.md
ERROR: YOUR SITE COULD NOT BE BUILT:
------------------------------------
Key excerpt cannot be set in the drop.
Is it possible to override this register's value in some way? Alternatively, is there a more Jekyll way to achieve this?
The Jekyll way to have a custom excerpt for your post (or any document in a collection), is to simply add it to the front matter:
---
title: Test Doc
excerpt: This is a custom excerpt
---
The opening paragraph of this document.
Another paragraph in this document.
Then reference it in your layout to render it:
<div class="post-excerpt">
{{ page.excerpt }}
</div>
or
<div>
{% for post in site.posts %}
<h2>{{ post.title }}</h2>
<div>{{ post.excerpt }}</div>
{% endfor %}
</div>

Parsing a nested tag, moving it outside of the parent, and changing its type using Nokogiri

I have HTML coming from an API that I want to clean up and format it.
I'm trying to get any <strong> tags that are the first element inside a <p> tag, and change it to be the parent of the <p> tag, and convert the <p> tag to <h4>.
For example:
<p><strong>This is what I want to pull out to an h4 tag.</strong>Here's the rest of the paragraph.</p>
becomes:
<h4>This is what I want to pull out to an h4 tag.</h4><p>Here's the rest of the paragraph.</p>
EDIT: Apologies for the nature of the question being too 'please write this for me'. I posted the solution I came up with below. I just had to take the time to really learn how Nokogiri works, but it is quite powerful and it seems like you can do almost anything with it.
doc = Nokogiri::HTML::DocumentFragment.parse(html)
doc.css("p").map do |paragraph|
first = paragraph.children.first
if first.element? and first.name == "strong"
first.name = 'h4'
paragraph.add_previous_sibling(first)
end
end

Ruhoh - Insert Tag every x items

I'm new to Ruby and Ruhoh and have I am trying to do something like "Rails each loop insert tag every 6 items?" but I am using Ruhoh.
Basically, I have a list of posts and every 3 posts I want to create a new row div.
I have looked through all the Ruhoh documentation and there doesn't appear to be an easy way to do this. I think I need to create a plugin in Ruhoh for a collection, but having no experience in ruby I don't really understand what I am doing. Any help or guidance in the right direction would be great,
Cheers.
I'm fairly new to ruby myself, however I think this solution meets your needs!
Create a new file in the plugin directory called pages_collection_view_addons.rb (if it doesn't already exist).
Add this to that file:
module PagesCollectionViewAddons
def chunks(n = 3)
# Get all the pages
pages = all
chunks = []
# Split the 'pages' array into chunks of size n
pages.each_slice(n) { |slice|
chunks.push({pieces: slice})
}
chunks
end
end
# Inform Ruhoh of this new addon
Ruhoh::Resources::Pages::CollectionView.send(:include, PagesCollectionViewAddons)
In your template add something such as:
{{# posts.chunks}}
<div class="row">
{{# pieces }}
<h1>{{ title }}</h1>
{{/ pieces }}
</div>
{{/ posts.chunks }}
This will iterate over each of the chunks where each chunk looks like:
{pieces: [post1, post2, post3]}
Hope this helps.

Get a hash into a Jekyll Liquid template from a Plugin for use in the FOR loop?

This one's got me stumped...
I'd like to share a YAML hash from a single file among a few other Jekyll pages.
I know you can put it in the Front Matter (which would require duplicating it), and I know you can generate (write) pages via a plugin (but I'm using it in a few different types of pages, which would be complex). Neither is what I'm looking for.
I'd like to loop over the hash with Liquid in my pages, but I can't seem to get the hash from the plugin to Liquid. {% capture %} only works with strings and {% assign %} won't let you call a tag within itself, like {% assign projects = gethash %} where gethash is a custom Liquid tag.
Basically, I'd like to use the separate YAML file like a text-based database.
YAML File has this in it:
projects:
category1:
-
title: Project 1
desc: Description
etc...
-
title: Project 2
etc...
category2:
-
title: Project 3
desc: Description
etc...
-
title: Project 4
etc...
Plugin is calling (which gives a Ruby Hash of the YAML):
def...
YAML::load(File.read('projects.yml'))
end...
And in template, I want to:
{% for p in projects %}
...
This should be really simple, but it's one of those Liquid things that is a pain.
How can you get a hash into Liquid from a plugin for use in the {% for %} loop?
Here's the solution that I came up:
A Jekyll Plugin that create's a Liquid Tag: yaml_to_liquid. This plugin parses the yaml file into a hash and then adds it to the Jekyll page variable.
module Jekyll
class YamlToLiquid < Liquid::Tag
def initialize(tag_name, arg, tokens)
super
if arg.length == 0
raise 'Please enter a yaml file path'
else
#yml_path = arg
end
end
def render(context)
yml = YAML::load(File.read(#yml_path))
context.registers[:page]['yml'] = yml
end
end
end
Liquid::Template.register_tag('yaml_to_liquid', Jekyll::YamlToLiquid)
To use it. Place the tag at the top of your .html or .md page just below the Yaml Front Matter and then access the yml variable as usual. This loop will only output the code hash (allows you to access the whole hash or just sub-hashes):
---
layout: page
---
{% yaml_to_liquid work/_projects.yml %}
<ul>
{% for n in page.yml.projects.code %}
<li>
{{ n.title }}
</li>
{% endfor %}
</ul>
Example of work/_projects.yml:
projects:
code:
- title:
url:
- title:
url:
websites:
- title:
url:
- title:
url:
Well, if you don't need it to be a plugin, it can be put in your _config.yml. For a plugin, you might need to append the hash to the site variable.
I think that a generator would suffice. There is a page about plugins that you should take a look.
This is what I'd use (I can't test right now, so it might be wrong!):
module Jekyll
class ProjectsGenerator < Generator
safe true
def generate(site)
# This probably won't work.
site.projects = YAML::load(File.read('projects.yml'))
end
end
end
Anyway, I really think that if you don't need the extra complexity (having a separate file, creating a new plugin just for you, etc), just put the data in _config.yml. Simplicity is good.
Hope that helps. :)

How do I grab content based on an outside tag match?

I am trying to organize a list of links and names based on a tag that is outside of the group of where the links and name reside. It's setup like so:
<h4>Volkswagen</h4>
<ul>
<li>beetle</li>
</ul>
<h4>Chevy</h4>
<ul>
<li>Volt / Electric</li>
</ul>
What I need is the result to be in the following format with the name as a link eventually but I can do that later if I can just get the items organized properly.
Each car brand could have multiple models of varying counts. I would need to organize them by car brand:
Volkswagen
Beetle Link Beetle
Jetta Link Jetta
Chevy
Volt Link Volt / Electric
S10 Link S10
I can get the list of brands with no problem. I am just having a hard time associating the batch of models with each brand as the <h4> tags aren't nested so I don't know how to associate them with the following <ul> list of cars.
I prefer to dive straight to each car, then back out to extract the car's brand:
cars = Hash.new { |h, k| h[k] = [] }
doc.xpath('//ul/li/a').each do |car|
brand = car.at('../../preceding-sibling::h4[1]').text
cars[brand] << {link: car['href'], name: car.text}
end
Note that the hash is initialized with a block specifying that the default value is an array. This allows appending hashes (via <<) as shown. The XPath ../../preceding-sibling::h4[1] says: go back up to the ul level and look back to the first preceding h4. This is the corresponding brand for the car.
Output:
{"Volkswagen"=>[
{:link=>"http://beetle.cars.com", :name=>"beetle"}
# others here
],
"Chevy"=>[
{:link=>"http://volt.cars.com", :name=>"Volt / Electric"}
# others here
]
}
I find this technique nice and simple, with just a single loop. Not everyone likes this style though.

Resources