Skip to content

Metalsmith plugin that establishes a family based on the files and folder structure in your source.

License

Notifications You must be signed in to change notification settings

tests-always-included/metalsmith-ancestry

Repository files navigation

metalsmith-ancestry

Builds a family tree from your file and folder locations. Lets you reference the next, previous, parent and all children of a particular resource. Very handy when used in conjunction with metalsmith-relative-links and [mustache-hbt-md], which lets you construct markdown with links to other pages, such as subpage listings or automatic navigation through pages.

npm version Build Status Dependencies Dev Dependencies codecov.io

What It Does

All of the file objects are assigned an .ancestry property, which is an object that contains the following properties:

fileObject.ancestry = {
    // These refer to just the file object itself
    basename: "index.html",  // The base filename of the source file.
    path: "test/index.html", // Full name of the source file.
    self: { ... },           // The file object that has this ancestry.

    // Up in the hierarchy.
    parent: { ... },         // Parent of this file object. **

    // Navigating member files of this folder.
    members: [ ... ],        // All file objects in the same folder.
    memberIndex: 0,          // The current file's index in members array.
    firstMember: { ... },    // The first member (same as .members[0]).
    lastMember: { ... },     // The last member.
    nextMember: { ... },     // The next member in the list.
    prevMember: { ... },     // The previous member in the list.

    // Jumping to other folders at the same level.
    siblings: [ ... ],       // One file object per folder. **
    siblingIndex: 2,         // The current file's index in siblings array.
    firstSibling: { ... },   // The first sibling (same as .siblings[0]).
    lastSibling: { ... },    // The last sibling.
    nextSibling: { ... },    // The next sibling in the list.
    prevSibling: { ... },    // The previous sibling in the list.

    // Descending in the directory tree.
    children: [ ... ],       // First member of each descendent. **
    childrenByName: { ... }, // First member of each descendent, by filename. **
    firstChild: { ... },     // The first child (same as .children[0]).
    lastChild: { ... }       // The last child.
}

** - These items are arrays that contain single file objects that "stand for" the entire folder. The file that is there is the first one sorted, so it would always appear as .members[0]. This is because one can not link to a folder because there are no folders in Metalsmith - only files.

Using this ancestry object, you can navigate to other file objects that would be supplied to any Metalsmith plugin. When you use metalsmith-relative-links and supply its link function a file object, it can return the URI necessary to link two resources together.

All files in a directory tree are placed together. Here's a sample directory listing.

index.html
site.css
about/index.html
contact/index.html
contact/email.html
contact/in-person.html
contact/live-chat/index.html
contact/live-chat/chat.jar
contact/live-chat/chat.swf
contact/location/index.html

If you were to inspect the .ancestry object that relates to contact/email.html, it would look something like the following. Please note that the example uses the shorthand «filename» to indicate we're linking to that specific file object.

{
    basename: "email.html",
    path: "contact/email.html",
    self: «contact/email.html»,

    // Up in the hierarchy.
    parent: «index.html»,
    root: «index.html»,

    // Navigating member files of this folder.
    memberIndex: 1,
    members: [
        «contact/index.html»
        «contact/email.html»
        «contact/in-person.html»
    ],
    firstMember: «contact/index.html»,
    lastMember: «contact/in-person.html»,
    nextMember: «contact/in-person.html»,
    prevMember: «contact/index.html»,

    // Jumping to other folders at the same level.
    siblingIndex: 1,
    siblings: [
        «about/index.html»,
        «contact/index.html»
    ],
    firstSibling: «about/index.html»,
    lastSibling: «contact/index.html»,
    nextSibling: null,
    prevSibling: «about/index.html»,

    // Descending in the directory tree.
    children: [
        «contact/live-chat/index.html»,
        «contact/location/index.html»
    ],
    childrenByName: {
        "live-chat": «contact/live-chat/index.html»,
        "location": «contact/location/index.html»
    },
    firstChild: «contact/live-chat/index.html»,
    lastChild: «contact/location/index.html»
}

A word of caution: if you were processing a file and you had access to its metadata, then .ancestry.self would be pointing back at itself. It's a circular link, so be very careful when you start dumping these objects to any logging system.

The real power shows up when you leverage these family links inside of your content. Combining metalsmith-hbt-md and metalsmith-relative-links, you can make a subpage listing. This example uses files that have title and summary in their file's frontmatter.

{{#ancestry.children}}
* [{{title}}]({{link.from ancestry.parent}}) - {{summary}}
{{/ancestry.children}}

If this was generated for the index.html within the above file tree, you would see this in the rendered markdown:

* [About Us](about/) - All about the creators of this site.
* [Contact Information](contact/) - The various ways you can reach us.

Another way to use this same information would be to see the amount of progress through a particular set of instructions. With a guide and each step being a page, you could use .memberIndex or .siblingIndex to determine how close the user is to completion.

You could also use this for instruction pages or in a gallery.

This example uses the #if helper from Handlebars and navigates pages in the same folder.
Link generation is done using metalsmith-relative-links.

{{#if ancestry.previousSibling}}
[Previous]({{link.to ancestry.previousSibling}})
{{/if}}

{{#if ancestry.nextSibling}}
[Next]({{link.to ancestry.nextSibling}})
{{/if}}


If you use another system, maybe metalsmith-mustache-metadata can help.
This example uses that plugin and links to folders that have a shared parent folder.
Link generation is done using metalsmith-relative-links.

{{#ancestry.previousMember?}}
[Previous]({{link.to ancestry.previousMember}})
{{/ancestry.previousMember?}}

{{#ancestry.nextMember?}}
[Next]({{link.to ancestry.nextMember}})
{{/ancestry.nextMember?}}

Installation

npm can do this for you.

npm install --save metalsmith-ancestry

Usage

Include this like you would include any other plugin. Here's a CLI example that also shows the default options. You don't need to specify any of these unless you want to change its value.

{
    "plugins": {
        "metalsmith-ancestry": {
            "ancestryProperty": "ancestry",
            "match": "**/*",
            "matchOptions": {},
            "reverse": false,
            "sortBy": null,
            "sortFilesFirst": "**/index.{htm,html,jade,md}"
        }
    }
}

And this is how you use it in JavaScript, with a small description of each option.

// Load this, just like other plugins.
var ancestry = require("metalsmith-ancestry");

// Then in your list of plugins you use it.
.use(ancestry())

// Alternately, you can specify options.  The values shown here are
// the defaults.
.use(ancestry({
    // Property name that gets the ancestry data object.
    ancestryProperty: "ancestry",

    // Pattern of files to match in case you want to limit processing
    // to specific files.
    match: "**/*",

    // Options for matching files.  See metalsmith-plugin-kit for
    // more information.
    matchOptions: {},

    // Reverse the sorting of siblings.
    reverse: false,

    // How to sort siblings.  See later documentation.
    sortBy: null,

    // Files that should be placed first, regardless.  Explained below.
    sortFilesFirst: "**/index.{htm,html,jade,md}"
})

This uses metalsmith-plugin-kit to match files. The .matchOptions object can be filled with options to control how files are matched.

Sorting Members

This is the most complex portion of the code. It is also the reason that sorting functions are exported with the plugin; those are covered later.

First, let's discuss the sortBy option. It is allowed to accept any of the following:

  • null - Don't worry about sorting and let the plugin do it for you automatically. The plugin sorts based on ancestry.path, case-insensitively.
  • "propertyName" - All single strings are treated as property names and are sorted case-insensitively.
  • ["propName1", "propName2"] - Sort by multiple property names. If the first property is identical in both file objects then it falls back to a second and third sort (as many as you define in the array).
  • function (a, b) {...} - Use the defined function for sorting file objects. You can compose one from functions you build and the sorting functions that the Ancestry module provides.

Because index files are typically the entry point and should be first, the sortFilesFirst property in the options allows you to configure this. It also allows several types of inputs.

  • null - This tells the plugin to sort with typical index files first. It matches index.htm, index.html, index.jade and index.md.
  • "**/index.html" - Strings match against the file using metalsmith-plugin-kit.filenameMatcher. So you can specify exact filenames or patterns with wildcards.
  • /(^|\/|\\)index\.([^.]*)/ - Regular expression objects are allowed. This one would match any file starting with index. with any extension but only one extension.
  • function (path) { ... } - Define your own function for the utmost in control.
  • [ matcher1, matcher2 ] - An array whose values are any of the above would also work. In this way you can easily match multiple globs or a complex series of behavior with several functions.

In addition, you can easily reverse the sort by setting reverse: true in the options.

Sorting Functions

On the ancestory function are some sorting properties. They are mostly exposed for easy testing but you are welcome to use them.

sortFunction = ancestry.sortByProperty(name)

Returns a function to sort file objects by a specific property name. The sorting uses ancestry.sortStrings() internally, so it is case insensitive.

files = [
    {
        contents: Buffer.from("Fake file contents"),
        title: "Spring"
    },
    {
        contents: Buffer.from("Fake file contents"),
        title: "Summer"
    },
    {
        contents: Buffer.from("Fake file contents"),
        title: "Fall"
    },
    {
        contents: Buffer.from("Fake file contents"),
        title: "Winter"
    }
];
files.sort(ancestry.sortByProperty("title"));
// Result order:
//   Fall, Spring, Summer, Winter

sortFunction = ancestry.sortCombine(functionListArray)

Array.prototype.sort only allows a single sorting function. .sortCombine() lets you take multiple sort functions and it will merge them together into a single function. If the first function returns a tie, the next one is used and so forth.

arr.sort(ancestry.sortCombine([
    firstSortFunction,
    secondSortFunction
]));

sortFunction = ancestry.sortReverse(originalSortFunction)

Reverses any sort.

arr = [ "test1", "Table", "ta" ];
arr.sort(ancestry.sortReverse(ancestry.sortStrings));
console.log(arr);  // [ "test1", "Table", "ta" ];

sortResult = ancestry.sortStrings(a, b)

Returns the value from sorting two strings, case-insensitively.

arr = [ "test1", "Table", "ta" ];
arr.sort(ancestry.sortStrings);
console.log(arr);  // [ "ta", "Table", "test1" ];

Upgrading

1.2.0 -> 1.3.0

The option .sortFilesFirst no longer can accept objects with a .match property. This appeared to be unused functionality.

If you used .matchOptions and that controled how globs were used with .sortFilesFirst, you now need to set the new property .sortFilesFirstOptions.

1.1.0 -> 1.2.0

Backwards compatible. Only added the .root property.

1.0.0 -> 1.1.0

Properties were renamed in order to avoid confusion with additional functionality. Change .first, .last, .next and.previous to .firstMember, .lastMember, .nextMember and .prevMember. Also, to make sure directories were the first-class citizens in the family tree, .siblings was renamed to .members.

API

metalsmith-ancestry

Metalsmith Ancestry - Generate a hierarchical listing of files based on where they are in the file tree. Add useful navigation metadata to files in your Metalsmith build.

Example

var ancestry = require("metalsmith-ancestry");

// Create your Metalsmith instance and add this like other middleware.
metalsmith.use(ancestry({
    // Options go here.
});

module.exports(options) ⇒ function

Factory to build middleware for Metalsmith.

Kind: Exported function
Params

  • options module:metalsmith-ancestry~options

module.exports~sortCombine(sorts) ⇒ function

Return a function that chains together multiple sort functions.

Kind: inner method of module.exports
Params

  • sorts Array.<function()>

module.exports~sortStrings(a, b) ⇒ number

Returns the right value from sorting two strings. Strings are sorted case-insensitively.

Kind: inner method of module.exports
Params

  • a string
  • b string

module.exports~sortReverse(sortFunction) ⇒ function

Reverses a sort function.

Kind: inner method of module.exports
Params

  • sortFunction function

module.exports~sortByProperty(propName) ⇒ function

Return a function that will sort file objects by a property. This will sort numbers appropriately as long as both values are numbers. Otherwise, this falls back to a string-based sort.

Kind: inner method of module.exports
Params

  • propName string

module.exports~sortByMatchingFilename(filesFirst, filesFirstOptions, ancestryProperty) ⇒ function

Files that match should be sorted first.

Kind: inner method of module.exports
Params

  • filesFirst module:metalsmith-plugin-kit~matchList
  • filesFirstOptions module:metalsmith-plugin-kit~matchOptions
  • ancestryProperty string

module.exports~buildSortFunction(sortBy, reverse, filesFirst, filesFirstOptions, ancestryProperty) ⇒ function

Create the sorting function using the options that were supplied.

Kind: inner method of module.exports
Params

  • sortBy ancestrySortBy
  • reverse boolean
  • filesFirst module:metalsmith-plugin-kit~matchList
  • filesFirstOptions module:metalsmith-plugin-kit~matchOptions
  • ancestryProperty string

module.exports~sortMembers(filesByFolder, sortFn)

Sort all members so they are in the right order before we determine the nextMember and prevMember links.

Kind: inner method of module.exports
Params

  • filesByFolder Object.<string, Array> - Ancestories grouped by their folder.
  • sortFn function - How children get sorted.

module.exports~assignParent(filesByFolder, ancestry)

Link to the parent if one exists.

Kind: inner method of module.exports
Params

  • filesByFolder Object.<string, Array> - Ancestories grouped by their folder.
  • ancestry ancestry

module.exports~assignRoot(options, ancestry)

Follow parent links up until there are no more, then link directly to the root.

Kind: inner method of module.exports
Params

  • options module:metalsmith-ancestry~options
  • ancestry ancestry

module.exports~assignRelativeLinks(ancestry, suffix, list)

Assigns the next, prev, first, last links for a given type.

Kind: inner method of module.exports
Params

  • ancestry ancestry
  • suffix string
  • list Array.<files>

module.exports~assignMemberLinks(ancestry)

Assign the member related links

Kind: inner method of module.exports
Params

  • ancestry ancestry

module.exports~assignChildren(filesByFolder, sortFn, options, ancestry)

Makes the list of children for each file object. The work is only done on the first member and copied to all subsequent members.

Kind: inner method of module.exports
Params

  • filesByFolder Object.<string, Array> - Ancestories grouped by their folder.
  • sortFn function - How children get sorted.
  • options module:metalsmith-ancestry~options
  • ancestry ancestry

module.exports~assignSiblings(options, ancestry)

Siblings is simply a copy of the parent's children.

Kind: inner method of module.exports
Params

  • options module:metalsmith-ancestry~options
  • ancestry ancestry

module.exports~assignIndexes(ancestry)

Assign siblingIndex and memberIndex

Kind: inner method of module.exports
Params

  • ancestry ancestry

module.exports~metalsmithFile : Object

This is a typical file object from Metalsmith.

Kind: inner typedef of module.exports
Properties

  • contents Buffer
  • mode string

module.exports~metalsmithFileMap : Object.<string, metalsmithFile>

An object whose values are metalsmithFile objects

Kind: inner typedef of module.exports

module.exports~ancestryOptions : Object

Options for this plugin.

Kind: inner typedef of module.exports
See: https://github.com/fidian/metalsmith-plugin-kit
Properties

  • ancestryProperty string - The metadata property name to assign
  • match module:metalsmith-plugin-kit~matchList - Files to match. Defaults to all files.
  • matchOptions module:metalsmith-plugin-kit~matchOptions - Options controlling globbing behavior.
  • reverse boolean
  • sortBy ancestrySortBy - How to sort siblings.
  • sortFilesFirst module:metalsmith-plugin-kit~matchList - What files should come first in the sibling list. Defaults to index files with htm, html, jade, or md extensions.
  • sortFilesFirstOptions module:metalsmith-plugin-kit~matchOptions - Options controlling the globbing behavior of sortFilesFirst.

module.exports~ancestry : Object

This represents the type of object that is added as metadata to each of the file objects.

Kind: inner typedef of module.exports
Properties

module.exports~ancestrySortBy : function | string | Array.<string> | null

Kind: inner typedef of module.exports

Development

This uses Jasmine, Istanbul and ESLint for tests.

# Install all of the dependencies
npm install

# Run the tests
npm run test

This plugin is licensed under the MIT License with an additional non-advertising clause. See the full license text for information.

About

Metalsmith plugin that establishes a family based on the files and folder structure in your source.

Resources

License

Stars

Watchers

Forks

Packages

No packages published