UNPKG

5.78 kBJavaScriptView Raw
1'use strict'
2
3const ware = require('ware')
4const fs = require('fs')
5const pug = require('pug')
6const join = require('path').join
7const url = require('url')
8const assign = Object.assign
9
10const hash = require('./lib/hash')
11const buildJs = require('./lib/build_js')
12const buildCss = require('./lib/build_css')
13
14const memoize = require('./lib/memoize')
15const noop = require('./lib/helpers/noop')
16const toArray = require('./lib/helpers/to_array')
17const eachCons = require('./lib/helpers/each_cons')
18const useCache = require('./lib/helpers/use_cache')
19
20/**
21 * Metalsmith middleware
22 */
23
24module.exports = function base (options) {
25 const ctx = {}
26
27 var app = ware()
28 .use(reset.bind(ctx))
29 .use(sortCss.bind(ctx))
30 .use(sortJs.bind(ctx))
31 .use(addJs.bind(ctx))
32 .use(addCss.bind(ctx))
33 .use(relayout.bind(ctx))
34
35 return function (files, ms, done) {
36 app.run(files, ms, done)
37 }
38}
39
40function reset (files, ms, done) {
41 this.styles = []
42 this.scripts = []
43 this.stylusImports = []
44 done()
45}
46
47/*
48 * Sorts out CSS into `styles` (local/external styles) and `stylusImports`
49 */
50
51function sortCss (files, ms, done) {
52 const list = toArray(ms.metadata().css)
53 const add = addAsset.bind(this, this.styles, 'css', files)
54
55 list.forEach((item) => {
56 if (item.match(/\.styl$/)) {
57 const path = join(ms.source(), item)
58 this.stylusImports.push(path)
59 } else {
60 add(item)
61 }
62 })
63
64 done()
65}
66
67function sortJs (files, ms, done) {
68 const list = toArray(ms.metadata().js)
69 const add = addAsset.bind(this, this.scripts, 'js', files)
70
71 list.forEach((item) => add(item))
72 done()
73}
74
75/**
76 * Internal: delegate of sortJs and sortCss.
77 * Adds a file into `this.styles` or `this.scripts`.
78 */
79
80function addAsset (list, what, files, item) {
81 const sources = files['_docpress.json'].sources
82
83 if (item.match(/^https?:\/\//)) {
84 list.push(item)
85 } else if (sources[item]) {
86 const local = sources[item]
87 list.push(local + '?t=' + hash(files[local].contents))
88 } else if (files[item]) {
89 list.push(item + '?t=' + hash(files[item].contents))
90 } else {
91 throw new Error(`${what}: can't find '${item}'`)
92 }
93}
94
95/**
96 * Assets
97 */
98
99function addCss (files, ms, done) {
100 const callback = (err, contents) => {
101 if (err) return done(err)
102 files['assets/style.css'] = {contents}
103 this.styles.unshift('assets/style.css?t=' + hash(contents))
104 done()
105 }
106
107 const cacheable = (this.stylusImports.length === 0)
108
109 ;(cacheable && useCache('cache/style.css', callback)) ||
110 buildCss({ imports: this.stylusImports }, callback)
111}
112
113/**
114 * Add JavaScript
115 */
116
117function addJs (files, ms, done) {
118 const callback = (err, contents) => {
119 if (err) return done(err)
120 files['assets/script.js'] = {contents}
121 this.scripts.push('assets/script.js?t=' +
122 hash(files['assets/script.js'].contents))
123
124 // Add user's files
125 let scripts = ms.metadata().scripts
126
127 if (Array.isArray(scripts)) {
128 let userScripts = scripts.map((location) => {
129 // Need to check if local or external
130 let fileUrl = url.parse(location, false, true)
131 if (fileUrl.host === null) {
132 // Local url
133 let file = files[location]
134 if (!file.contents) return
135 let fileHash = hash(file.contents)
136 return `${location}?t=${fileHash}`
137 } else {
138 // External url
139 return `${location}`
140 }
141 }).filter((url) => !!url)
142 this.scripts = this.scripts.concat(userScripts)
143 }
144
145 done()
146 }
147
148 useCache('cache/script.js', callback) ||
149 buildJs({}, callback)
150}
151
152/**
153 * Layout pug.
154 * Passes these template options:
155 *
156 * * `base` — prefix.
157 * * `toc` — the table of contents, as per toc.json.
158 * * `index` — the index, as per index.json.
159 * * `meta` — metalsmith metadata.
160 * * `prev.title` — previous page title.
161 * * `prev.url` — previous page url.
162 * * `next.title` — next page title.
163 * * `next.url` — next page url.
164 * * `active` — the filename of the active file (eg, `foo/index.html`)
165 * * `styles` — array of CSS files
166 * * `scripts` — array of JavaScript files
167 *
168 * `meta` typically has:
169 *
170 * * `github` (Optional)
171 * * `docs`
172 */
173
174function injectDisqus (disqus) {
175 if (!disqus) return noop
176 var exclude = new RegExp(disqus.exclude || 'index')
177
178 function addDisqus (fname, meta) {
179 if (exclude.test(fname)) return null
180 return disqus.shortname
181 }
182
183 return addDisqus
184}
185
186function relayout (files, ms, done) {
187 const toc = files['_docpress.json'].toc
188 const index = files['_docpress.json'].index
189 const meta = ms.metadata()
190
191 const addDisqus = injectDisqus(meta.disqus)
192 const pugData = fs.readFileSync(join(__dirname, 'data/layout.pug'), 'utf-8')
193 const layout = memoize(['pug', pugData], () => {
194 return pug.compile(pugData, { pretty: true })
195 })
196
197 eachCons(index, (_, fname, __, prevName, ___, nextName) => {
198 if (!fname.match(/\.html$/)) return
199 const file = files[fname]
200 const base = Array(fname.split('/').length).join('../')
201 const styles = this.styles.map(relativize(base))
202 const scripts = this.scripts.map(relativize(base))
203
204 const locals = {
205 base,
206 toc,
207 index,
208 meta,
209 styles,
210 scripts,
211 prev: prevName && assign({}, index[prevName], { url: base + prevName }),
212 next: nextName && assign({}, index[nextName], { url: base + nextName }),
213 active: fname
214 }
215
216 meta.disqus = addDisqus(fname, meta)
217 const key = [ pugData, locals, file ]
218
219 file.contents = memoize(['pugdata', key], () => {
220 return layout(assign({}, file, locals))
221 })
222 })
223
224 done()
225}
226
227function relativize (base) {
228 return function (url) {
229 if (url.substr(0, 1) === '/' || url.match(/^https?:\/\//)) {
230 return url
231 }
232
233 return base + url
234 }
235}