Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Use custom classifications like tags & categories #670

Closed
wants to merge 3 commits into from

9 participants

@goodtouch

We used to classify posts with tags & categories.

This patch adds a generic way to define & use your own classifications through site.config[:classifications] by adding tags like behavior to site & posts for added classifications.

Let's say you want classify Post by referring to Project(s), Customer(s) and Layout(s) you can now simply add the following to _config.yml :

classifications: 
  - projects
  - customers
  - layouts

And start using those classifications like you would with categories or tags :

Add the following to a Post:

project: bartender
layout:
  - grid
  - responsive

And then use the following in any template :

<h2>Projects</h2>
<ul>
{% for project in site.projects %}
  <li>{{ project | first }}</li>
{% endfor %}
</ul>
@jnovack

I love this idea. I was actively looking for a way to make "namespaces" and I stumbled upon this.

I wanted to have 'projects' (detailed pages with updates and run-throughs) and a 'notes' (tips and tricks hodge-podge) section without making them 'posts' but still be liquified and available under site. for iteration.

@agarie

I like this idea, but I'm not sure it should be in the core of jekyll - the guys maintaining it want to remain "simple". What do you think, @parkr?

@jnovack

What I get out of this, is the ability to separate "posts" (post is defined as an item that is processed through jekyll and available under site.).

Sure, you can make static pages, but they don't get the same processing and you cannot perform any iteration functions on them.

Most sites I visited through the examples have static pages which could probably be better served under a different configuration namespace (I'm using the term 'namespace' as used by media wiki, posts across namespaces are separate but equal (although some are more equal than others)). Here is an example:

  • When adding a new page to their "projects", they have to add it to the menu. If the project .md file was within the site.projects it could be iterated and automatically added to the menu and other pages which list all projects.

  • 'Blog posts' are traditionally akin to a newspaper article, publish once. 'Wiki' notes are different, notes should be updated and removed with the times. And project pages probably somewhere in between. Although, I can have a wiki note, blog post, and project page about a single subject (read: category).

The "classification" allows for a person to develop their OWN culture on their site. I think it's a good first start towards something bigger.

(disclaimer: I think this is a good start, however, more needs to be fleshed out with this process as it is a paradigm shift. if you use the "notes" and "projects" mentality, you should not have to date the file. e.g. _posts/ use date, _notes/ do not.)

@goodtouch

@agarie: I first planned to do this in a jekyll plugin but couldn't find a nice way do to it without monkey patching some inner classes of Jekyll (and that would have been a pain to maintain).

Yet I've tried to keep the changes I made in the core quite simple, easy to read and backward compatible : basically you've got an array that stores classifications (defaults to tags & categories) and use it to allow referring those attributes like you did with tags & categories.

If anyone wants to do this an other way, I'm open to suggestions :smiley:

@goodtouch

Updated to latest mojombo/jekyll master

@kelvinst

I like the idea too. It is a good idea to made classifications as we need... And the use of Jekyll was not affected by this changes, once that @goodtouch take care to keep the general bahavior of Jekyll mading tags and categories the default classifications...

And, in my opinion, this doesn't make Jekyll more complex, remembering that the changes "keep the easy things easy and make the hard things possible" - as @mattr- says in other comment

lib/jekyll/post.rb
@@ -10,6 +10,7 @@ class << self
# Valid post name regex.
MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
+ # FIXME
@kelvinst
kelvinst added a note

OK, I saw what to fix here (maybe a comment of what to fix will help)... But I hope that was the best way to do this (concat the array below to the classificatons list in Post#to_liquid).

But maybe you remove categories and tags from this array will help... :+1:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/jekyll/post.rb
@@ -63,7 +68,16 @@ def initialize(site, source, dir, name)
self.published = self.published?
self.populate_categories
@kelvinst
kelvinst added a note

Why keep this? If you remove the next if classification == 'categories' below doesn't work? This way seems worse to me.

I did this to keep the original behavior of categories:

self.categories = dir.downcase.split('/').reject { |x| x.empty? }
# ...
if self.categories.empty?
  self.categories = self.data.pluralized_array('category', 'categories').map {|c| c.to_s.downcase}
end
self.categories.flatten!

which is different from the one used for tags (that I also use for all classifications):

self.tags = self.data.pluralized_array("tag", "tags").flatten

Having the same behavior for both would clean things up in my patch but I imagine the init, conditional assignation and .to_s.downcase are here for a good reason?

Yet I should probably rewrite this part in a populate classifications method.

I see now, sorry.

Maybe we can do this a little better, but now my mind doesn't see it. :wink:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kelvinst

Someone else? I'm feeling lonely here :cry:

@mattr-
Owner

Could some of the use cases presented here be solved with the new data sources feature that just recently landed in master?

I'm sure I'm just not seeing it but the need for classifications above tags and categories doesn't seem necessary since what you put in the front matter is not restricted, which means you can get what you want w/o any changed to jekyll's core.

I'll think about it a bit more, but in the meantime, this at least needs to be rebased on to current master before it gets further consideration. Thanks!

@goodtouch

Hi all,

Sorry for the delay.

I'll rebase this and check the new data sources feature as soon as I get some time.

The main point of classifications was to provide a way to list and find posts according to one of the front matter attributes like we can do for tags or categories (with site.tags or site.tags[mytag]).

The ideas behind this are to :

  • make it easy to classify posts according to many criteria
  • index them on different pages easily after that.

It's easy in jekyll to do a {% for tag in site.tags %} but it becomes harder if you try to represent a hierarchy or try to get all tags used with one particular tag to build one.

I had 2 real use cases for this :

Use case 1: Provide multiples search/browse axes

Use jekyll to index html/css demos and allow users to browse and find them by:

  • project,
  • library (or html framework),
  • component,
  • or basic html5 elements

where each demo could be indexed in multiple components or elements.

This is not a "released" project but you can still access it here: http://3fc04cdc75b4e12a5c151830447c47f0494ffe86.with-style.goodtouch.org/ and use top navigation to get the idea of what I'm talking about.

By using the following configuration :

classifications:
- components
- elements
- projects
- libraries

and using the following example front matter:

layout: design
title: Big Round Scrollbars
category: components/scrollbars
element: scrollbars   

or

layout: design                 
title: Github Buttons          
category: components/buttons   
elements: buttons              
component: buttons             
library: github                

The top level navigation becomes quite simple:

{% for project in site.projects %} 
  <li><a href="{{site.baseurl}}/projects/{{project | first }}" title="{{project | first}}">{{ project | first }}</a></li>
{% endfor %}
...
{% for element in site.elements %} 
  <li><a href="{{site.baseurl}}/elements/{{element | first }}" title="{{element | first}}">{{ element | first }}</a></li>
{% endfor %}
etc...

Sublevel Navigation becomes:

{% for item in site.elements[page['element']] limit:40 offset:0 %}
  <li>     
    <a href="{{site.baseurl}}{{item.url}}" title="{{item.title}}">{{item.title}}</a>
  </li>
{% endfor %}

or

{% for item in site.projects[page['project']] limit:40 offset:0 %}
  <li>
    <a class="article-link" href="{{site.baseurl}}{{item.url}}" title="{{item.title}}">{{item.title}}</a>
  </li>
{% endfor %}

Use case 2: Nested hierarchy

An other use case was in a documentation website, using classifications to build a hierarchy that looks like:

doc
├── internal doc
│   ├── "chapter" (ie. Guidelines)
│   │   └── section (Coding Style)
│   │   │   └── document (C)
├── public doc
└── ...

Here we use the following config file:

classifications:
  - internals
  - section
  - docs

We'll then use internal: to define internal chapters and doc: for the public chapters.

Here is an example front matter:

layout:     internal-docs
title:      C guidelines
internal:   Guidelines
section:    Coding Style
permalink:  /internal-docs/guidelines/c.html

Then it's easy to build a main top-nav for the internal part (or the public part) with:

{% if page.docs %}
  ...  nice public nav here ...
{% elsif page.internal %}
  {% for tabs in site.internals %}
    {% assign tab = tabs | first %} 
    {% assign first_post = site.internals[tab] | last %} 
    <li class="{% if page.internal contains tab %}selected{% endif %}">
      <a href="{{ first_post.url }}">{{ tab }}</a>
    </li>
  {% endfor %}
{% endif %}

and here is a simplified version of a side bar grouping posts in sections for a chapter
(this one will work as long as you don't use the "section" classification to classify public doc but it's easy to use something) :

{% assign sections = (site.internals[page.internal] | map:'section' | uniq ) %}
{% for section in sections %}
  <li><h2><span>{{section}}</span></h2></li>
  <ul>
    {% for post in site.sections[section] reversed %}
      <li class="{% if page.url == post.url %}selected{% endif %}">
        <a href="{{ post.url }}"><span>{{ post.title }}</span></a>
      </li>
    {% endfor %}
  <ul>
{% endfor %}

Sorry for the long post, I hope it makes it easier to see use cases solved with this feature.

@mattr-
Owner

:heart_eyes: This. is. awesome! I'm really really excited about this now. Thank you for writing such great examples.

We have to be careful of one very important thing and that's backwards compatibility. As long as we can maintain the same behavior that tags and categories currently have, then we're on the path to beautiful blue skies and warm sunny beaches with this thing.

Thanks again for such great examples. I really want to see this rebased soon so we can take a closer look at it.

lib/jekyll/post.rb
@@ -63,7 +68,16 @@ def initialize(site, source, dir, name)
self.published = self.published?
self.populate_categories
- self.populate_tags
+
+ @site.classifications.each do |classification|
+ next if classification == 'categories'
+ self.send(
+ "#{classification}=".to_sym,
+ self.data.pluralized_array(ActiveSupport::Inflector.singularize(classification), classification).flatten
+ )
+ # self.tags = self.data.pluralized_array("tag", "tags").flatten

So, particularly, I don't like commented code snippets. Someone else agree?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@troyswanson

Would there be any way to provide the system with some hints around sorting? Given your example around a documentation site, how will you define which chapter is the first one?

@goodtouch

You're welcome :smiley:

I've pushed a cleaner rebased version so that everyone can comment & review.
(categories and tags should still work as expected)

lib/jekyll/site.rb
@@ -25,6 +27,13 @@ def initialize(config)
self.file_read_opts = {}
self.file_read_opts[:encoding] = config['encoding'] if config['encoding']
+ self.classifications = %w(categories tags)
+ self.classifications |= (config['classifications'] || []).reject { |c| c !~ /\A[a-z][a-zA-Z_-]*\z/ || FORBIDDEN_CLASSIFICATIONS.include?(c) }
+
+ self.classifications.each do |classification|
+ self.class.send :attr_accessor, classification
+ end

Just one question: you don't need to call classification.to_sym here? If not, then you can resume this for decalration to just self.class.send :attr_accessor, classifications.

You're right, this should work with self.class.send :attr_accessor, *classifications. I'll push this one soon :wink:

Awesome (:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/test_generated_site.rb
@@ -14,7 +14,7 @@ class TestGeneratedSite < Test::Unit::TestCase
end
should "ensure post count is as expected" do
- assert_equal 36, @site.posts.size
+ assert_equal 37, @site.posts.size

What is the real intention of this test? I think these tests that must be "updated" on each file addition so, soooo annoying. In my opinion, if it really make sense test this, we must make this test more change safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kelvinst

Totally revised. Totaly AWESOME :heart: :sparkles: :blue_heart: :cake: :green_heart: :star2: :purple_heart: :rooster: :yellow_heart:

@goodtouch

@troyswanson : I've been using different way to sort classifications including:

  • use and chose alpha wisely (like for tags and categories)
  • use a plugin and something like a float in the front-matter
  • use the post date to build the list ({% assign sections = (site.internals[page.internal] | map:'section' | uniq ) %} or {% for post in site.sections[section] reversed %})
  • use a static ordered list in the config file

Now that I've checked the data source feature mentioned earlier, I think I would try to use it in addition to classifications because this would make it simple for non developer contributors of the doc to edit a static "navigation tree" yaml config file while still being simple and generic in the rendering template.

Using the date to hint the order (if you're not concerned about chronological order, like in a documentation website) works pretty well and also sort the files in the directory.

@ghost

I use Jekyll a lot and this has been something I've wanted for quite some time now. Wonderful!

I'm not entirely clued in on everything that has been discussed here, I'm still quite new to Github, however I'm just trying to figure out whether this has been merged into the main code base yet and whether I'm able to use it on my blog at this point?

@parkr
Owner

@louistxt No, this has not been merged yet. You'll see "Merged" at the top of this page on the right instead of "Open".

lib/jekyll/post.rb
@@ -80,8 +82,15 @@ def populate_categories
self.categories.flatten!
end
- def populate_tags
- self.tags = self.data.pluralized_array("tag", "tags").flatten
+ def populate_classifications
+ @site.classifications.each do |classification|
+ next if classification == 'categories'
+ self.send(
+ "#{classification}=".to_sym,
+ self.data.pluralized_array(ActiveSupport::Inflector.singularize(classification), classification).flatten
+ )
@parkr Owner
parkr added a note

This block contains some security concerns for Pages.

For now I only added this feature to Posts, but seing your comment in #1168 I think this can be added to Pages too (I didn't checked the source yet, but I can have a look if you want).

I tried to mitigate the security concerns by adding the following:

FORBIDDEN_CLASSIFICATIONS = %w(posts pages html_pages categories tags).freeze
# ...
self.classifications |= (config['classifications'] || []).reject { |c| c !~ /\A[a-z][a-zA-Z_-]*\z/ || FORBIDDEN_CLASSIFICATIONS.include?(c) }

If I'm totally missing the point about what kind of security concerns you see here don't hesitate to give a more specific example (it's been a long time and I might not be up to date :grin:)

@mattr- Owner
mattr- added a note

The only security concern I'm aware of is the conversion of the string to a symbol. Since symbols aren't garbage collected and you're symbolizing the classifications for every post, this can cause a DoS if the machine running Jekyll runs out a memory (I'm thinking a large site with a lot of posts)

@mattr- Owner
mattr- added a note

@goodtouch I think @parkr was talking about GitHub Pages, not pages in Jekyll. :smiley:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ben-rosio

+1, this is exactly what I need to do, so I can separate works from blog posts, but possibly have the same tags/categories. Hope to see it merged in soon

@mattr-
Owner

@goodtouch Could you rebase this on to current master for us please?

@parkr Could you elaborate on the security concerns please? :smiley_cat:

@cirosantilli

+1. Would be useful to have for pages also in addition to posts.

@parkr parkr closed this
@parkr parkr reopened this
@mattr- mattr- was assigned by parkr
@goodtouch

@mattr- Do you still want a rebase ? I might have some time to work on it this week if you want.

@mattr-
Owner

@goodtouch A rebase would be :sparkles: awesome :sparkles:

@mattr- mattr- added this to the 2.1 milestone
@mattr-
Owner

I'd like to push this off until 2.1. The merge for this is very messy and I may just end up re-implementing it all (with credit to @goodtouch, of course)

@goodtouch

@mattr- : I won't let you do the dirty work for me :smile:.
Just tell me when would you want this one so that I can get organized to finish the rebase?

@parkr
Owner

@goodtouch If you want to help with Collections so we can move Pages and Posts to collections, then your solution here should be worlds simpler (you'll only have to do it once! Yay!)

@goodtouch

@parkr Yeah ! I just had a look at the commits you did for Collections while rebasing and thought it would be awesome to also have classifications inside.

Rebasing (for post) is almost done.
I was planning to update this pull request to have something clean here, then move forward to also support this in Collections.

@goodtouch

Here is the rebased version :tada:!

Those commits should pass the test suite :wink:.

There are still some areas to clean up (see FIXME if interested) but it should be enough to start testing and talking about it.

I reworked some parts to make them look a little bit more like recent code in Jekyll (preparing to migrate to Collections!).

lib/jekyll/post.rb
((22 lines not shown))
def populate_categories
- if categories.empty?
- self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase}
+ if classifications["categories"].empty?
+ classifications["categories"] = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase}
@parkr Owner
parkr added a note

Why not use your categories= method here?

I thought it would be nice to factorize tags and categories as they could be seen as specializations of classifications.
As the others (including tags) are now grouped in the classifications hash why not add categories too ?

Having the same behavior would also make it easier to merge populate_categories code with populate_classifications and avoid code duplication as the only difference between categories and other classifications is that they are downcased to generate nice folders & urls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/jekyll/post.rb
((30 lines not shown))
end
- def populate_tags
- self.tags = Utils.pluralized_array_from_hash(data, "tag", "tags").flatten
+ def populate_classifications
+ populate_categories
+ @site.classifications.keys.each do |classification|
+ next if classification == 'categories'
+ value = Utils.pluralized_array_from_hash(data, ActiveSupport::Inflector.singularize(classification), classification).flatten
@parkr Owner
parkr added a note

I don't think we have ActiveSupport available at runtime (it's a dev dependency for something in the test suite I think). Perhaps we should add it to the runtime deps?

I changed the gemspec to add this one.
(But I guess your comment wasn't for me ?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/jekyll/site.rb
@@ -97,6 +99,26 @@ def collections
end
end
+ # FIXME: complete doc
+ # The list of classifications and their corresponding Jekyll::Post instances.
+ #
+ # Returns a Hash containing classifications name-to-posts pairs.
+ def classifications
+ if !@classifications
+ # Include categories and tags for backward compatibility
+ @classifications = %w(categories tags)
+ # Sanitize classifications
+ @classifications |= (config['classifications'] || []).reject { |c| c !~ /\A[a-z][a-zA-Z_-]*\z/ || FORBIDDEN_CLASSIFICATIONS.include?(c) }
+ # FIXME: Define instance methods (maybe we can avoid this: statically define tags & categories and use hash for the rest?)
+ # But should check if we can still easily filter (liquid) through hash first
+ @classifications.each do |classification|
+ self.class.module_eval %{def #{classification}; post_attr_hash('#{classification}'); end}, __FILE__, __LINE__
@parkr Owner
parkr added a note

I don't think we need this, right? If we just want classifications passed through to liquid, why would this be necessary, given the @classifications hash?

The first implementation didn't used the @classifications hash and created accessors for each classification.

I changed this to a @classifications hash to mirror what you did for collections, to fix security concerns about creating dynamic accessors and to be able to easily remove dynamic accessors if needed.

I also think that it would be great to only use the hash as it wouldn't change anything in the templates thanks to the site_payload.

Yet we would loose the ability to do stuff like site.projects in plugins (like we can do for categories and tags) and would have to use site.classifications['projects'] and I wasn't sure about this.

I put a FIXME and kept the accessors to smoothly upgrade my existing sites to the hash method and to remove this if it doesn't look too weird :bowtie:.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@parkr
Owner

Thanks for the rebase! :sparkles:

It would be great to take care of the FIXMEs before we merge. It also feels a bit like this is a re-implementation of the idea of collections, but for Posts and Pages...

@goodtouch

You're welcome!

Of course I will handle the FIXMEs but I thought it could be interesting to discuss/tweak some of them here before going for a specialized implementation :grin:.

If I understood what you did for Collections by browsing the commits, I think Collections and Classifications are distinct concepts that would fit very well together.

You may be able to get similar behavior by "hacking" one or the other but I think it would make much sense to have them both working together :

Let's say for example that you would have a Collection of graphical_assets, each one being Classified by colors, projects, element type etc... and an other Collection of source_code_examples, Classified by language, performance etc...

You would be able to access them by doing something like site.collections['graphical_assets'].colors['blue'] or to browse them and create a menu with site.collections['graphical_assets'].classifications...

This is essentially the behavior I get by using one classification to define something similar to the collection type inside a Post but I think it would be much more cleaner & user friendly if we were having a customizable top level element like Collections as it would create a simple hierarchy that covers 80% of the use cases (like the Use Case 2 I described earlier in this Pull Request) while not preventing some more advanced usages if needed.

I think this would be freaking awesome :heart_eyes:

@mattr- mattr- was unassigned by parkr
@parkr parkr self-assigned this
@parkr parkr removed their assignment
@mattr- mattr- was assigned by parkr
@goodtouch

Here is the clean rebased version :dizzy:!

@parkr No more module_eval or FIXMEs in this one :wink:.

@goodtouch

(I guess travis had a problem during the CI on ruby 2.0.0. The same code passed here: https://travis-ci.org/goodtouch/jekyll/builds/25087467 and ran nicely)

@mattr- mattr- was unassigned by parkr
@parkr parkr self-assigned this
@parkr
Owner

Once again the big green merge button is gone :(

goodtouch added some commits
@goodtouch goodtouch Use custom classifications (tags, categories, ...)
We used to classify posts with tags & categories.

This patch adds a generic way to define & use your own classifications through `site.config[:classifications]` by adding `tags` like behavior to site & posts for added classifications.

Let's say you want classify Post by referring to Project(s), Customer(s) and Layout(s) you can now simply add the following to `_config.yml` :

```
classifications:
  - projects
  - customers
  - layouts
```

And start using those classifications like you would with `categories` or `tags` :

Add the following to a Post:

```
projects:
  - project1
  - project2
layout:
  - grid
  - responsive
```

And then use the following in any template :

```
<h2>Projects</h2>
<ul>
{% for project in site.projects %}
  <li>{{ project | first }}</li>
{% endfor %}
</ul>
```

You can also access classifications through `classifications` methods of `Site` and `Post` if you're not in a Liquid template context (ie. when writting a Jekyll plugin) :

```
module Jekyll
  class CustomGenerator
    def generate(site)
      related_layouts = {}
      site.classifications['projects'].each do |project, posts|
        related_layouts[project] = posts.map{ |post| post.classifications['layouts'] }.flatten
      end
      # ...
    end
  end
end
```
de5a9b1
@goodtouch goodtouch Add unit tests c8bc63f
@goodtouch goodtouch Add cucumber test 7966ada
@goodtouch

Let's make it green again then :)

@parkr parkr modified the milestone: 2.2, 2.1
@parkr
Owner

So now that we've added collections, with the hope that we can organize content based on its membership in a given collection, where do you stand on this?

@jnovack

I just saw 2.3.0 get posted, what is the disposition at this time?

@parkr
Owner

@jnovack No, this is not.

@jnovack

Frownie face. But thank you for answering so quickly. Here's hoping I can stop monkeypatching every time I upgrade. :/

@goodtouch

@jnovack I can rebase this on the latest master if you need.

@parkr I think I'll start working on adding this feature to Collections if it's good for you ?

(I wrote a comment up in this thread (April 18) about why it would be nice to also have Classifications in Collections if you want)

@parkr
Owner

@goodtouch Please open a new PR for collections if that's cool. Just use Document. Before Jekyll 3, posts will be Document-based, so it'll all fall into place. Thanks!

@parkr parkr closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 22, 2014
  1. @goodtouch

    Use custom classifications (tags, categories, ...)

    goodtouch authored
    We used to classify posts with tags & categories.
    
    This patch adds a generic way to define & use your own classifications through `site.config[:classifications]` by adding `tags` like behavior to site & posts for added classifications.
    
    Let's say you want classify Post by referring to Project(s), Customer(s) and Layout(s) you can now simply add the following to `_config.yml` :
    
    ```
    classifications:
      - projects
      - customers
      - layouts
    ```
    
    And start using those classifications like you would with `categories` or `tags` :
    
    Add the following to a Post:
    
    ```
    projects:
      - project1
      - project2
    layout:
      - grid
      - responsive
    ```
    
    And then use the following in any template :
    
    ```
    <h2>Projects</h2>
    <ul>
    {% for project in site.projects %}
      <li>{{ project | first }}</li>
    {% endfor %}
    </ul>
    ```
    
    You can also access classifications through `classifications` methods of `Site` and `Post` if you're not in a Liquid template context (ie. when writting a Jekyll plugin) :
    
    ```
    module Jekyll
      class CustomGenerator
        def generate(site)
          related_layouts = {}
          site.classifications['projects'].each do |project, posts|
            related_layouts[project] = posts.map{ |post| post.classifications['layouts'] }.flatten
          end
          # ...
        end
      end
    end
    ```
  2. @goodtouch

    Add unit tests

    goodtouch authored
  3. @goodtouch

    Add cucumber test

    goodtouch authored
This page is out of date. Refresh to see the latest.
View
13 features/site_data.feature
@@ -79,6 +79,19 @@ Feature: Site data
Then the _site directory should exist
And I should see "Yuengling" in "_site/index.html"
+ Scenario: Use the custom site.projects classification variable
+ Given I have a _posts directory
+ And I have an "index.html" page that contains "{% for post in site.projects.bartender %} {{ post.content }} {% endfor %}"
+ And I have a configuration file with "classifications" set to:
+ | value |
+ | projects |
+ And I have the following posts:
+ | title | date | project | content |
+ | Beer List | 2009-03-26 | bartender | Our most famous beverages |
+ When I run jekyll build
+ Then the _site directory should exist
+ And I should see "Our most famous beverages" in "_site/index.html"
+
Scenario: Order Posts by name when on the same date
Given I have a _posts directory
And I have an "index.html" page that contains "{% for post in site.posts %}{{ post.title }}:{{ post.previous.title}},{{ post.next.title}} {% endfor %}"
View
2  jekyll.gemspec
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
s.extra_rdoc_files = %w[README.markdown LICENSE]
s.add_runtime_dependency('liquid', "~> 2.5.5")
+ s.add_runtime_dependency('activesupport', "~> 3.2.13")
s.add_runtime_dependency('classifier', "~> 1.3")
s.add_runtime_dependency('listen', "~> 2.5")
s.add_runtime_dependency('kramdown', "~> 1.3")
@@ -55,7 +56,6 @@ Gem::Specification.new do |s|
s.add_development_dependency('simplecov-gem-adapter', "~> 1.0.1")
s.add_development_dependency('coveralls', "~> 0.7.0")
s.add_development_dependency('mime-types', "~> 1.5")
- s.add_development_dependency('activesupport', '~> 3.2.13')
s.add_development_dependency('jekyll_test_plugin')
s.add_development_dependency('jekyll_test_plugin_malicious')
s.add_development_dependency('rouge', '~> 1.3')
View
1  lib/jekyll.rb
@@ -27,6 +27,7 @@ def require_all(path)
require 'kramdown'
require 'colorator'
require 'toml'
+require 'active_support/inflector'
# internal requires
require 'jekyll/version'
View
2  lib/jekyll/convertible.rb
@@ -226,6 +226,8 @@ def write(dest)
def [](property)
if self.class::ATTRIBUTES_FOR_LIQUID.include?(property)
send(property)
+ elsif self.respond_to?(:classifications) && self.classifications.keys.include?(property)
+ self.classifications[property]
else
data[property]
end
View
54 lib/jekyll/post.rb
@@ -12,11 +12,10 @@ class Post
dir
date
id
- categories
next
previous
- tags
path
+ classifications
]
# Attributes for Liquid templates
@@ -35,7 +34,7 @@ def self.valid?(name)
attr_accessor :site
attr_accessor :data, :extracted_excerpt, :content, :output, :ext
- attr_accessor :date, :slug, :tags, :categories
+ attr_accessor :date, :slug, :classifications
attr_reader :name
@@ -52,7 +51,8 @@ def initialize(site, source, dir, name)
@base = containing_dir(source, dir)
@name = name
- self.categories = dir.downcase.split('/').reject { |x| x.empty? }
+ self.classifications = {'categories' => dir.downcase.split('/').reject { |x| x.empty? }}
+
process(name)
read_yaml(@base, name)
@@ -64,8 +64,7 @@ def initialize(site, source, dir, name)
self.date = Time.parse(data["date"].to_s)
end
- populate_categories
- populate_tags
+ populate_classifications
end
def published?
@@ -76,15 +75,37 @@ def published?
end
end
- def populate_categories
- if categories.empty?
- self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase}
- end
- categories.flatten!
+ # Delegate accessors to @classifications for backward compatibility
+ def categories=(c)
+ @classifications ||= {}
+ @classifications['categories'] = c
+ end
+
+ def categories
+ @classifications ||= {}
+ @classifications['categories']
+ end
+
+ def tags=(t)
+ @classifications ||= {}
+ @classifications['tags'] = t
+ end
+
+ def tags
+ @classifications ||= {}
+ @classifications['tags']
end
- def populate_tags
- self.tags = Utils.pluralized_array_from_hash(data, "tag", "tags").flatten
+ def populate_classifications
+ @site.classifications.keys.each do |classification|
+ value = Utils.pluralized_array_from_hash(data, ActiveSupport::Inflector.singularize(classification), classification)
+
+ if classification == 'categories'
+ value = self.categories.empty? ? value.map { |c| c.to_s.downcase } : self.categories
+ end
+
+ classifications[classification] = value.flatten
+ end
end
# Get the full path to the directory containing the post files
@@ -278,6 +299,13 @@ def destination(dest)
path
end
+ # Convert this post into a Hash for use in Liquid templates.
+ #
+ # Returns the representative Hash.
+ def to_liquid(attrs = nil)
+ Utils.deep_merge_hashes(super(attrs), @classifications)
+ end
+
# Returns the shorthand String identifier of this Post.
def inspect
"<Post: #{id}>"
View
76 lib/jekyll/site.rb
@@ -111,6 +111,27 @@ def collection_names
end
end
+ # The list of classifications and their corresponding Jekyll::Post instances.
+ #
+ # Returns a Hash containing classifications name-to-posts pairs.
+ def classifications
+ Hash[classification_names.map { |classification| [classification, post_attr_hash(classification)]} ]
+ end
+
+ # The list of classification names.
+ #
+ # Returns an array of classification names from the configuration
+ # or ["categories", "tags"] if the `classifications` key is not set.
+ def classification_names
+ if !@classifications
+ # Include categories and tags for backward compatibility
+ @classifications = %w(categories tags)
+ # Sanitize classifications
+ @classifications |= (config['classifications'] || []).reject { |c| c !~ /\A[a-z][a-zA-Z_-]*\z/ }
+ end
+ @classifications
+ end
+
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
@@ -295,7 +316,13 @@ def post_attr_hash(post_attr)
# Build a hash map based on the specified post attribute ( post attr =>
# array of posts ) then sort each array in reverse order.
hash = Hash.new { |h, key| h[key] = [] }
- posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
+ posts.each do |p|
+ if p.classifications.keys.include?(post_attr) && !p.respond_to?(post_attr.to_sym)
+ p.classifications[post_attr].each { |t| hash[t] << p }
+ else
+ p.send(post_attr.to_sym).each { |t| hash[t] << p }
+ end
+ end
hash.values.each { |posts| posts.sort!.reverse! }
hash
end
@@ -319,16 +346,19 @@ def site_data
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
- # "time" - The Time as specified in the configuration or the
- # current time if none was specified.
- # "posts" - The Array of Posts, sorted chronologically by post date
- # and then title.
- # "pages" - The Array of all Pages.
- # "html_pages" - The Array of HTML Pages.
- # "categories" - The Hash of category values and Posts.
- # See Site#post_attr_hash for type info.
- # "tags" - The Hash of tag values and Posts.
- # See Site#post_attr_hash for type info.
+ # "time" - The Time as specified in the configuration or the
+ # current time if none was specified.
+ # "posts" - The Array of Posts, sorted chronologically by post date
+ # and then title.
+ # "pages" - The Array of all Pages.
+ # "html_pages" - The Array of HTML Pages.
+ # "classifications" - The Hash of classification names and Hash of classification values and Posts.
+ # "categories" - The Hash of category values and Posts.
+ # See Site#post_attr_hash for type info.
+ # "tags" - The Hash of tag values and Posts.
+ # See Site#post_attr_hash for type info.
+ # For each custom classification, the Hash will also contain a key:
+ # "#{classification}" - The Hash of custom classification values and Posts.
def site_payload
{
"jekyll" => {
@@ -336,18 +366,18 @@ def site_payload
"environment" => Jekyll.env
},
"site" => Utils.deep_merge_hashes(config,
- Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}], {
- "time" => time,
- "posts" => posts.sort { |a, b| b <=> a },
- "pages" => pages,
- "static_files" => static_files.sort { |a, b| a.relative_path <=> b.relative_path },
- "html_pages" => pages.reject { |page| !page.html? },
- "categories" => post_attr_hash('categories'),
- "tags" => post_attr_hash('tags'),
- "collections" => collections,
- "documents" => documents,
- "data" => site_data
- }))
+ Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}],
+ Utils.deep_merge_hashes(classifications, {
+ "time" => time,
+ "posts" => posts.sort { |a, b| b <=> a },
+ "pages" => pages,
+ "static_files" => static_files.sort { |a, b| a.relative_path <=> b.relative_path },
+ "html_pages" => pages.reject { |page| !page.html? },
+ "collections" => collections,
+ "classifications" => classifications,
+ "documents" => documents,
+ "data" => site_data
+ })))
}
end
View
7 test/source/_posts/2013-05-28-classifications.md
@@ -0,0 +1,7 @@
+---
+title: Some Tags
+projects:
+- bartender
+---
+
+Our most famous beverages
View
2  test/test_generated_site.rb
@@ -14,7 +14,7 @@ class TestGeneratedSite < Test::Unit::TestCase
end
should "ensure post count is as expected" do
- assert_equal 42, @site.posts.size
+ assert_equal 43, @site.posts.size
end
should "insert site.posts into the index" do
View
7 test/test_post.rb
@@ -15,7 +15,7 @@ def do_render(post)
context "A Post" do
setup do
clear_dest
- stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
+ stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS.merge({'classifications' => %w(projects)}) }
@site = Site.new(Jekyll.configuration)
end
@@ -531,6 +531,11 @@ def do_render(post)
assert_equal "Empty YAML.", post.content
end
+ should "recognize custom project classification in yaml" do
+ post = setup_post("2013-05-28-classifications.md")
+ assert post.classifications['projects'].include?('bartender')
+ end
+
context "rendering" do
setup do
clear_dest
View
7 test/test_site.rb
@@ -45,7 +45,7 @@ class TestSite < Test::Unit::TestCase
context "creating sites" do
setup do
stub(Jekyll).configuration do
- Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
+ Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'classifications' => %w(projects)})
end
@site = Site.new(Jekyll.configuration)
@num_invalid_posts = 2
@@ -77,6 +77,7 @@ def generate(site)
before_layouts = @site.layouts.length
before_categories = @site.categories.length
before_tags = @site.tags.length
+ before_projects = @site.classifications['projects'].length
before_pages = @site.pages.length
before_static_files = @site.static_files.length
before_time = @site.time
@@ -86,6 +87,7 @@ def generate(site)
assert_equal before_layouts, @site.layouts.length
assert_equal before_categories, @site.categories.length
assert_equal before_tags, @site.tags.length
+ assert_equal before_projects, @site.classifications['projects'].length
assert_equal before_pages, @site.pages.length
assert_equal before_static_files, @site.static_files.length
assert before_time <= @site.time
@@ -213,10 +215,13 @@ def generate(site)
posts = Dir[source_dir("**", "_posts", "**", "*")]
posts.delete_if { |post| File.directory?(post) && !Post.valid?(post) }
categories = %w(2013 bar baz category foo z_category publish_test win).sort
+ projects = %w(bartender)
assert_equal posts.size - @num_invalid_posts, @site.posts.size
assert_equal categories, @site.categories.keys.sort
+ assert_equal projects, @site.classifications['projects'].keys.sort
assert_equal 5, @site.categories['foo'].size
+ assert_equal 1, @site.classifications['projects']['bartender'].size
end
context 'error handling' do
Something went wrong with that request. Please try again.