-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgulp-nav.coffee
140 lines (133 loc) · 5.87 KB
/
gulp-nav.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
###
copyright (c) 201{4,5} Jess Austin <jess.austin@gmail.com>, MIT license
gulp-nav is a gulp plugin to help build navigation elements. gulp-nav adds
"nav" objects to vinyl file objects. nav objects contain titles, (relative)
href links, active flags, parents, children, and siblings. The last two
properties are arrays, which may optionally be ordered. For other than default
behavior, call the exported function with an object that defines one or more
of the following options (the first five can be a single property name, or an
array of property names):
sources
targets
titles
orders
skips
hrefExtension
demoteTopIndex
###
{basename} = require 'path'
through = require 'through2'
{relative, resolve} = require './web-path'
module.exports = ({sources, targets, titles, orders, skips, hrefExtension,
demoteTopIndex}={}) ->
# defaults -- the first five are just arrays of property names
sources ?= ['data', 'frontMatter']
targets ?= ['nav', 'data.nav']
titles ?= ['short_title', 'title']
orders ?= 'order'
skips ?= 'skipThis'
hrefExtension ?= 'html'
demoteTopIndex ?= no
# single options don't have to come wrapped in an Array
sources = [ sources ] unless Array.isArray sources
targets = [ targets ] unless Array.isArray targets
titles = [ titles ] unless Array.isArray titles
orders = [ orders ] unless Array.isArray orders
skips = [ skips ] unless Array.isArray skips
# scaffolding for crawling the directory structure
files = []
navTree =
parent: null
children: {}
exists: no
title: null
orderGen = 9999
root = {}
through.obj (file, encoding, transformCallback) ->
# if vinyl objects have different properties, take first that exists
source = (file[source] for source in sources).reduce (x, y) -> x ?= y
source ?= file # just look for properties on the vinyl obj itself
title = (source[title] for title in titles).reduce (x, y) -> x ?= y
order = (source[order] for order in orders).reduce (x, y) -> x ?= y
# skip this file?
for skip in skips
if skip of source and source[skip]
return transformCallback null, file
# normalize the path and break it into its constituent elements
path = resolve '/', file.relative
.replace /index\.[^/]+$/, '' # index identified with directory
.replace /\.[^./]+$/, '.' + hrefExtension # e.g. '.jade' -> '.html'
# find the right spot for the new resource
current = navTree
for element in (path.split /([^/]*\/)/ # e.g. '/a/b' -> ['/', 'a/', 'b']
.filter (element) -> element isnt '')
current = current.children[element] ?= # recurse down the path, filling
parent: current # in tree with missing elements
children: {}
exists: no # for directories without index
title: basename element.replace /\/?index[^/]*$/, ''
.toLowerCase()
.replace /\.[^.]*$/, '' # remove extension
.replace /[-._]/g, ' ' # punctuation to spaces
.replace /\b\w/g, (first) ->
first.toUpperCase() # capitalize each word
.replace /^$/, '/' # root needs a title too
order: orderGen++
# clean up the leaf
current.exists = yes # if we're here, this resource *does* exist!
current.title = title ? current.title # overwrite defaults with non-nulls
current.order = order ? current.order
# use leaf to make the nav object
nav = navInContext current, [path], root
# set properties of vinyl object XXX does this need error handling?
for target in targets
obj = file
[properties..., last] = target.split '.' # for nested target props
obj = obj[property] ?= {} for property in properties
obj[last] = nav
# delay until we've seen them all...
files.push file
transformCallback()
, (flushCallback) -> # (still in the call to through.obj)
for name, child of navTree.children # there's only one
root.obj = child
root.name = name
if demoteTopIndex # top-level index becomes sibling of its children
for name, child of root.obj.children
navTree.children[resolve root.name, name] = child
child.parent = navTree
root.obj.children = {}
# ...and now we've seen them all
@push file for file in files
flushCallback()
# Create the actual nav object that will be exposed to user code. This object
# knows (and the objects that it creates, in turn, know) the context in which
# it is exposed, so that it can expose accurate link information. Accessor
# properties are used because the structure is circular so we need some
# laziness.
navInContext = (nav, context, root) ->
postFix = if context[-1..][0][-1..] in ['/', '.'] then '..' else '.'
Object.defineProperties
title: nav.title
# how you get here from there, but only if here is an actual place
href: relative context[0], resolve context... if nav.exists
active: context[0] is resolve context... # ending where we started?
,
parent:
enumerable: yes # these properties should be easy to find
get: -> # they're accessors because we need lazy eval
navInContext nav.parent, context.concat postFix
children:
enumerable: yes
get: ->
(navInContext child, context.concat name for [child, name] in (
[child, name] for name, child of nav.children)
.sort ([a, ...], [b, ...]) -> a.order - b.order)
siblings:
enumerable: yes
get: ->
@parent.children
root:
enumerable: yes
get: ->
navInContext root.obj, context.concat root.name