Search code examples
rubymiddleman

Middleman parent, siblings, children methods


When creating pages in middleman, how do I indicate which pages are parents/siblings/children? The documentation gives some indication how you can use parent siblings and children methods to build navigation and breadcrumbs, but it doesn't say how to arrange the pages in the directory so that they respond to these methods (parent, siblings, children) in the appropriate way.

Each resource in the sitemap is a Resource object. Pages can tell you all kinds of interesting things about themselves. You can access frontmatter data, file extension, source and output paths, a linkable url, its mime type, etc. Some of the properties of the Page are mostly useful for Middleman's rendering internals, but you could imagine filtering pages on file extension to find all .html files, for example.

Each page can also find other pages related to it in the site hierarchy. The parent, siblings, and children methods are particularly useful in building navigation menus and breadcrumbs.

This is the parent method http://rubydoc.info/github/middleman/middleman/Middleman/Sitemap/Extensions/Traversal#parent-instance_method

This is the the children method http://rubydoc.info/github/middleman/middleman/Middleman/Sitemap/Extensions/Traversal#children-instance_method

This is the siblings method

http://rubydoc.info/github/middleman/middleman/Middleman/Sitemap/Extensions/Traversal#siblings-instance_method


Solution

  • After a bit of spelunking into Middleman's code, I found a Cucumber test that describes the #parent, #children, and #siblings methods.

    middleman / middleman-core / features / sitemap_traversal.feature:

    Feature: Step through sitemap as a tree
    
      Scenario: Root
        Given the Server is running at "traversal-app"
        When I go to "/index.html"
        Then I should see "Path: index.html"
        Then I should not see "Parent: index.html"
        Then I should see "Child: sub/index.html"
        Then I should see "Child: root.html"
        Then I should not see "Child: proxied.html"
    
    ...continued... 
    
    • In short, it seems like the parent resource can be found one level up with the file name 'index.html,' so if you're viewing /foo/bar/some_resource.html, its parent can be found at /foo/index.html.
    • Its siblings are at the same level as the request (note that "eponymous directories" get converted into an 'index file', eg. /foo/bar/ becomes /foo/bar.html, making its siblings anything at the /foo/* level.)
    • Its children are at the level below.

    By placing any file in their respective positions in your file hierarchy, you should be able to reference that file, or a set which includes that file, by calling #parent, #children, or #siblings.

    Be warned while reading the tests that there are a couple "fake" routes (/sub/fake.html and fake2.html, and /directory-indexed/fake.html and fake2.html) that have been set up in the config file.


    A deeper dive

    If you can't take the Cucumber tests at face value (and I don't blame you), there's more! After all, what is this, "I should see" and "I should not see" nonsense? Well, there's an answer for that.

    In the fixtures for the test (middleman/middleman-core/fixtures/traversal-app/), layout.erb is the only file with any contents whatsoever. In it, you can see that the Child, Parent, and Sibling paths are printed out in the body of the html document.

    middleman / middleman-core / fixtures / traversal-app / source / layout.erb:

    Path: <%= current_page.path %>
    
    Source: <%= current_page.source_file.sub(root + "/", "") %>
    
    <% if current_page.parent %>
      Parent: <%= current_page.parent.path %>
    <% end %>
    
    ...continued...
    

    This helps to explain the tests, which are simply looking for strings in the response body, originating from the layout. You can see the test's exact behavior in the Cucumber step definitions (middleman / middleman-core / lib / middleman-core / step_definitions /).

    Finally, the layout uses the methods that we set out to describe in the first place, #parent, #children, and #siblings, which are defined in middleman-core.

    In middleman / middleman-core / lib / middleman-core / sitemap / extensions / traversal.rb:

    module Traversal
      # This resource's parent resource
      # @return [Middleman::Sitemap::Resource, nil]
      def parent
        parts = path.split("/")
        parts.pop if path.include?(app.index_file)
    
        return nil if parts.length < 1
    
        parts.pop
        parts << app.index_file
    
        parent_path = "/" + parts.join("/")
    
        store.find_resource_by_destination_path(parent_path)
      end
    
      # This resource's child resources
      # @return [Array<Middleman::Sitemap::Resource>]
      def children
        return [] unless directory_index?
    
        if eponymous_directory?
          base_path = eponymous_directory_path
          prefix    = %r|^#{base_path.sub("/", "\\/")}|
        else
          base_path = path.sub("#{app.index_file}", "")
          prefix    = %r|^#{base_path.sub("/", "\\/")}|
        end
    
        store.resources.select do |sub_resource|
          if sub_resource.path == self.path || sub_resource.path !~ prefix
            false
          else
            inner_path = sub_resource.path.sub(prefix, "")
            parts = inner_path.split("/")
            if parts.length == 1
              true
            elsif parts.length == 2
              parts.last == app.index_file
            else
              false
            end
          end
        end
      end
    
      # This resource's sibling resources
      # @return [Array<Middleman::Sitemap::Resource>]
      def siblings
        return [] unless parent
        parent.children.reject { |p| p == self }
      end
    
      ...continued...
    end
    

    I'll leave off explaining the Ruby code, as I've got to hurry off to work. If you'd like me to investigate further, let me know in the comments and I'll get back at it.