From 17cd5ac2a81a848a258d8227137abd4fcb2d202b Mon Sep 17 00:00:00 2001 From: "Gabriel J. Csapo" Date: Tue, 10 Apr 2018 17:50:46 -0700 Subject: [PATCH] 1.2.0 - plugins don't have to have a parse or render method to be interpreted. - makes sure content, type and collection is always returned even if no options block is parsed from string - crawl will inject outputPath into all pages that are rendered - inMemory is now an option on Site that doesn't build the site to disk but rather to memory - onBuild can now be set on Site that will trigger after a build has been finished - reduces the complexity of bin operations. All Site based functionality is done at the top level instead of each function having its own instance, it is shared. - when the server fails it will throw the stack output not the error.toString() this is not really helpful - moves linting to standard --- .eslintignore | 2 - .eslintrc | 18 - CHANGELOG.md | 11 + bin/sweeney.js | 206 ++--- docs/assets/realtime-editing.png | Bin 0 -> 89992 bytes docs/code/Site.html | 8 +- docs/code/Template.html | 794 ------------------ docs/code/generate.js.html | 95 --- docs/code/global.html | 457 ---------- docs/code/index.html | 2 +- docs/code/module-lib_generate.html | 328 -------- ...le-lib_util.html => module-lib_util_.html} | 176 +++- docs/code/site.js.html | 150 ++-- docs/code/template.js.html | 161 ---- docs/code/util.js.html | 320 ++++--- docs/index.html | 27 + example/index.sy | 2 +- example/layouts/default.sy | 2 +- lib/defaultPlugins.js | 112 ++- lib/mimes.js | 2 +- lib/serve/serve.css | 77 ++ lib/serve/serve.js | 301 +++++++ lib/site.js | 148 ++-- lib/util.js | 318 ++++--- package.json | 17 +- test/defaultPlugins.js | 81 +- test/site.js | 276 +++--- test/util.js | 526 ++++++------ 28 files changed, 1762 insertions(+), 2855 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 docs/assets/realtime-editing.png delete mode 100644 docs/code/Template.html delete mode 100644 docs/code/generate.js.html delete mode 100644 docs/code/global.html delete mode 100644 docs/code/module-lib_generate.html rename docs/code/{module-lib_util.html => module-lib_util_.html} (85%) delete mode 100644 docs/code/template.js.html create mode 100644 lib/serve/serve.css create mode 100644 lib/serve/serve.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7432c9a..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -docs diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5296df3..0000000 --- a/.eslintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "eslint:recommended", - "env": { - "es6": true, - "node": true - }, - "rules": { - "quotes": ["error", "single"], - "semi": ["error", "always"], - "no-inner-declarations": 0, - "no-cond-assign": 0, - "prefer-const": ["error"] - }, - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module" - } -} diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd06fb..3fb30f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 1.2.0 (04/10/2018) + +- plugins don't have to have a parse or render method to be interpreted. +- makes sure content, type and collection is always returned even if no options block is parsed from string +- crawl will inject `outputPath` into all pages that are rendered +- inMemory is now an option on Site that doesn't build the site to disk but rather to memory +- `onBuild` can now be set on Site that will trigger after a build has been finished +- reduces the complexity of bin operations. All Site based functionality is done at the top level instead of each function having its own instance, it is shared. +- when the server fails it will throw the stack output not the error.toString() `this is not really helpful` +- moves linting to standard + # 1.1.0 (02/27/2018) - fixes new command (the path to the example/new project directory was wrong) diff --git a/bin/sweeney.js b/bin/sweeney.js index 1d21a79..f6d1747 100755 --- a/bin/sweeney.js +++ b/bin/sweeney.js @@ -1,25 +1,18 @@ #!/usr/bin/env node -const http = require('http'); -const fs = require('fs'); -const path = require('path'); -const woof = require('woof'); -const { promisify } = require('util'); +const fs = require('fs') +const path = require('path') +const woof = require('woof') -const { version } = require('../package.json'); +const { version } = require('../package.json') -const Site = require('../lib/site'); +const Site = require('../lib/site') +const Serve = require('../lib/serve/serve') -const readFile = promisify(fs.readFile); - -const mimes = require('../lib/mimes'); -const { ms, getConfig, copyDirectory, renderSubDepends } = require('../lib/util'); +const { ms, getConfig, copyDirectory, renderSubDepends } = require('../lib/util') process.on('unhandledRejection', (error) => { console.error(`Error: \n ${error.stack}`); // eslint-disable-line -}); - -// this is used when establishing when a build occured -let build = Date.now(); +}) const program = woof(` Usage: sweeney [options] @@ -50,16 +43,16 @@ const program = woof(` flags: { source: { type: 'string', - validate: function(value) { - const stats = fs.statSync(value); - return !stats.isDirectory() ? `please provide a valid directory path. \n ${value} is not a valid path.` : true;// ensure that this is a directory + validate: function (value) { + const stats = fs.statSync(value) + return !stats.isDirectory() ? `please provide a valid directory path. \n ${value} is not a valid path.` : true// ensure that this is a directory } }, output: { type: 'string', - validate: function(value) { - const stats = fs.statSync(value); - return !stats.isDirectory() ? `please provide a valid directory path. \n ${value} is not a valid path.` : true;// ensure that this is a directory + validate: function (value) { + const stats = fs.statSync(value) + return !stats.isDirectory() ? `please provide a valid directory path. \n ${value} is not a valid path.` : true// ensure that this is a directory } }, port: { @@ -71,148 +64,107 @@ const program = woof(` default: false } } -}); +}) -if(program['help'] || program['version']) { - process.exit(0); +if (program['help'] || program['version']) { + process.exit(0) } -async function getConfigInDirectory() { - const config = await getConfig(program.source ? path.resolve(process.cwd(), program.source) : process.cwd()); +async function getConfigInDirectory () { + const config = await getConfig(program.source ? path.resolve(process.cwd(), program.source) : process.cwd()) - config.source = program.source ? path.resolve(process.cwd(), program.source) : config.source ? path.resolve(process.cwd(), config.source) : process.cwd(); - config.output = program.output ? path.resolve(process.cwd(), program.output) : config.output ? path.resolve(process.cwd(), config.output) : path.resolve(process.cwd(), 'site'); + config.source = program.source ? path.resolve(process.cwd(), program.source) : config.source ? path.resolve(process.cwd(), config.source) : process.cwd() + config.output = program.output ? path.resolve(process.cwd(), program.output) : config.output ? path.resolve(process.cwd(), config.output) : path.resolve(process.cwd(), 'site') - return config; + return config } -(async function() { +(async function () { try { - if(program.new) { - const start = process.hrtime(); - const directory = process.cwd(); + if (program.new) { + const start = process.hrtime() + const directory = process.cwd() - await copyDirectory(path.resolve(__dirname, '..', 'example'), directory); - const end = process.hrtime(start); + await copyDirectory(path.resolve(__dirname, '..', 'example'), directory) + const end = process.hrtime(start) - console.log(`application bootstrapped in ${directory} [${ms(((end[0] * 1e9) + end[1]) / 1e6)}]`); // eslint-disable-line + process.stdout.write(`application bootstrapped in ${directory} [${ms(((end[0] * 1e9) + end[1]) / 1e6)}]`) + process.exit(1) } - let config = await getConfigInDirectory(); + let config = await getConfigInDirectory() + // Only build to the virtual file system when we are serving content + // Don't enable if build and serve and done together + config.inMemory = program.watch && !program.build + + const site = new Site(config) - if(program.build) { - const start = process.hrtime(); + if (program.build) { + const start = process.hrtime() - const site = new Site(config); - await site.build(); + await site.build() - if(config.include) { + if (config.include) { config.include.forEach((i) => { - copyDirectory(path.resolve(config.source, i), config.output + i.substr(i.lastIndexOf('/'), i.length)); - }); + copyDirectory(path.resolve(config.source, i), config.output + i.substr(i.lastIndexOf('/'), i.length)) + }) } - const end = process.hrtime(start); + const end = process.hrtime(start) // replace the source path with nothing so that we don't get a bunch of duplicate strings process.stdout.write(` site built at ${config.output} [${ms(((end[0] * 1e9) + end[1]) / 1e6)}] ${site.rendered.map((top) => '\n' + renderSubDepends(top, 0).replace(new RegExp(config.source + '/', 'g'), '')).join('')} - `.trim() + '\n\n'); + `.trim() + '\n\n') } - if(program.serve) { - const server = http.createServer(async (req, res) => { - if(req.url === '/__api/update') { - res.statusCode = 200; - return res.end(build.toString()); - } - let file = req.url || '/index.html'; - if(file === '/') file = '/index.html'; - const ext = file.substr(file.lastIndexOf('.') + 1, file.length); - - try { - // removing the leading / from the file name - let contents = (await readFile(path.resolve(config.output, file.substr(1, file.length)))).toString('utf8'); - // inject javascript into the page to refresh it in the case that a new build occurs - if(ext == 'html' && program.watch !== undefined) { - contents = contents.replace('', ``); - } - - res.writeHead(200, { - 'Content-Type': mimes[ext] - }); - res.end(contents); - } catch(ex) { - res.statusCode = 500; - res.end(); - } - }).listen(program.port, () => { - process.stdout.write(`sweeney listening on http://localhost:${server.address().port}\n`); - }); - } + if (program.serve) { + try { + await site.build() - if(program.watch) { - let start = process.hrtime(); + let server = await Serve({ + port: program.port, + watch: program.watch, + site + }) - let site = new Site(config); - await site.build(); - let end = process.hrtime(start); - process.stdout.write(`site built at ${config.output} [${ms(((end[0] * 1e9) + end[1]) / 1e6)}] and watching ${config.source}\n`); + process.stdout.write(`sweeney listening on http://localhost:${server.address().port}\n`) + process.stdout.write('\u001B[90mremember to issue build after you done making changes to write to disk\u001B[39m\n') + } catch (ex) { + process.stdout.write(`server failed to start with error: \n ${ex.stack} \n`) + } + } + + if (program.watch) { + process.stdout.write(`sweeney watching ${site.source} \n`) - fs.watch(config.source, { + fs.watch(site.source, { recursive: true - }, async function(ev, file) { + }, async function (ev, file) { // refresh the require cache in the case config has updated - if(file.indexOf('.sweeney') > -1) { - delete require.cache[require.resolve(path.resolve(config.source, '.sweeney'))]; - config = await getConfigInDirectory(); - site = new Site(config); - await site.build(); + if (file.indexOf('.sweeney') > -1) { + delete require.cache[require.resolve(path.resolve(site.source, '.sweeney'))] + site.config = await getConfigInDirectory() + await site.build() } // we don't want to rebuild the output directory because this is expected to change - if(file.substring(0, file.lastIndexOf('/')) !== config.output.substring(config.output.lastIndexOf('/') + 1, config.output.length) && file.indexOf('.git') === -1) { - start = process.hrtime(); - await site.build(); - build = Date.now(); - end = process.hrtime(start); + if (file.substring(0, file.lastIndexOf('/')) !== site.output.substring(site.output.lastIndexOf('/') + 1, site.output.length) && file.indexOf('.git') === -1) { + const start = process.hrtime() + + await site.build() + + const end = process.hrtime(start) + process.stdout.write(` site rebuilt in [${ms(((end[0] * 1e9) + end[1]) / 1e6)}] because of ${ev} of ${file} ${site.rendered.map((top) => '\n' + renderSubDepends(top, 0).replace(new RegExp(config.source + '/', 'g'), '')).join('')} - `.trim() + '\n\n'); + `.trim() + '\n\n') } - }); + }) } - } catch(ex) { - process.stdout.write(`Error: \n ${ex.stack} \n`); + } catch (ex) { + process.stdout.write(`Error: \n ${ex.stack} \n`) } -}()); +}()) diff --git a/docs/assets/realtime-editing.png b/docs/assets/realtime-editing.png new file mode 100644 index 0000000000000000000000000000000000000000..acedeadd4ac9a1f66e29055b90801f4f6622fe18 GIT binary patch literal 89992 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ðJ9B9LLkwY+f{$Xj~s4eWOy^K+! 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
@@ -252,7 +252,7 @@

(static) build<
Source:
@@ -356,7 +356,7 @@

(static) c
Source:
@@ -467,7 +467,7 @@

(static) dataSource:
diff --git a/docs/code/Template.html b/docs/code/Template.html deleted file mode 100644 index 1bf1476..0000000 --- a/docs/code/Template.html +++ /dev/null @@ -1,794 +0,0 @@ - - - - - - Template - Documentation - - - - - - - - - - - - - - - - - -
- -

Template

- - - - - - - -
- -
- -

- Template -

- - -
- -
-
- - -
- - - -

new Template(template, directory, output)

- - - - - -
-

handles the state of a template and can render it

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
template - - -String - - - - -

a string containing the template

- -
directory - - -String - - - - -

directory of where template if from

- -
output - - -String - - - - -

the output directory to store assets

- -
- - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - - - -

Methods

- - - -
- - - -

(static) attributesToHTML(attributes) → {String}

- - - - - -
-

takes an attributes object and turns it into a html string

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
attributes - - -Object - - - - -

an object containing the html attributes

- -
- - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -String - - -
-
- - -
-
    -
  • html attributes string
  • -
-
- - -
- - - -
- - -
- - - -

(static) include(file, attributes) → {String}

- - - - - -
-

includes an asset or template from within a template

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
file - - -String - - - - -

relative path from file to asset

- -
attributes - - -Object - - - - -

object with attribute keys that will turn into html

- -
- - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -String - - -
-
- - -
-
    -
  • compiled template or asset tag
  • -
-
- - -
- - - -
- - -
- - - -

(static) render(data) → {string}

- - - - - -
-

renders the template to string

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - - -Object - - - - -

object that contains state for template

- -
- - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -string - - -
-
- - -
-
    -
  • compiled template
  • -
-
- - -
- - - -
- - - - - - -
- -
- - - - -
- -
- - - - - - - \ No newline at end of file diff --git a/docs/code/generate.js.html b/docs/code/generate.js.html deleted file mode 100644 index 41fb3fa..0000000 --- a/docs/code/generate.js.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - generate.js - Documentation - - - - - - - - - - - - - - - - - -
- -

generate.js

- - - - - - - -
-
-
/**
- * @module lib/generate
- */
-
-const fs = require('fs');
-const path = require('path');
-const { promisify } = require('util');
-
-const writeFile = promisify(fs.writeFile);
-
-const { renderTemplate, getTemplates, ensureDirectoryExists } = require('./util');
-
-/**
- * generates a site given the directory and config object
- * @method generate
- * @param  {String} directory - the directory where site files are contained
- * @param  {Object=} config    - contains overrides to build scripts
- * @param  {String} config.output - output override for built files
- */
-module.exports = async function generate(directory, config) {
-  const output = config.output ? path.resolve(directory, config.output) : path.resolve(directory, 'site');
-
-  // make sure the output directory exists
-  await ensureDirectoryExists(output);
-
-  const posts = await getTemplates(`${directory}/posts`);
-  const root = await getTemplates(directory);
-
-  root.concat(posts).forEach(async function(post) {
-    // TODO: expose related articles
-    // TODO: expose next articles (can be configurable by default 3)
-
-    await writeFile(`${output}/${post.file}.html`, await renderTemplate(directory, post.path, Object.assign({ content: post.content, posts, post, page: {}, site: {} }, config), output));
-  });
-};
-
-
-
- - - - -
- -
- - - - - - - diff --git a/docs/code/global.html b/docs/code/global.html deleted file mode 100644 index 849778b..0000000 --- a/docs/code/global.html +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - Global - Documentation - - - - - - - - - - - - - - - - - -
- -

Global

- - - - - - - -
- -
- -

- -

- - -
- -
-
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - - - - - - - - - - - -

Methods

- - - -
- - - -

build() → {Promise}

- - - - - -
-

using the source directory, crawl all the necessary template files that will be ingested compiled

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -Promise - - -
-
- - - -
- - - -
- - -
- - - -

categorize() → {Object}

- - - - - -
-

returns back pages and layouts as uncompiled templates

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -Object - - -
-
- - -
-
    -
  • returns an object with the keys layout and pages that are hashmaps
  • -
-
- - -
- - - -
- - -
- - - -

data() → {Object}

- - - - - -
-

dynamically retrieves the files content -appropriately puts top level collections as root level keys

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
-
Returns:
- - - -
-
- Type: -
-
- -Object - - -
-
- - - -
- - - -
- - - - - - -
- -
- - - - -
- -
- - - - - - - \ No newline at end of file diff --git a/docs/code/index.html b/docs/code/index.html index 519ee49..d9a4ad1 100644 --- a/docs/code/index.html +++ b/docs/code/index.html @@ -24,7 +24,7 @@
diff --git a/docs/code/module-lib_generate.html b/docs/code/module-lib_generate.html deleted file mode 100644 index c86b679..0000000 --- a/docs/code/module-lib_generate.html +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - lib/generate - Documentation - - - - - - - - - - - - - - - - - -
- -

lib/generate

- - - - - - - -
- -
- - - -
- -
-
- - - - - -
- - - - - - - - - - - - - - -

Methods

- - - -
- - - -

(inner) generate(directory, configopt)

- - - - - -
-

generates a site given the directory and config object

-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDescription
directory - - -String - - - - - - - - - - -

the directory where site files are contained

- -
config - - -Object - - - - - - <optional>
- - - - - -
-

contains overrides to build scripts

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
output - - -String - - - - -

output override for built files

- -
- - -
- - - - - - - - - - - - - - - - -
- - - - - - -
- -
- - - - -
- -
- - - - - - - \ No newline at end of file diff --git a/docs/code/module-lib_util.html b/docs/code/module-lib_util_.html similarity index 85% rename from docs/code/module-lib_util.html rename to docs/code/module-lib_util_.html index e22fbc2..90a6fc9 100644 --- a/docs/code/module-lib_util.html +++ b/docs/code/module-lib_util_.html @@ -24,7 +24,7 @@
@@ -576,7 +576,7 @@

(inner
Source:
@@ -738,7 +738,7 @@

(inner) mergeSource:
@@ -935,7 +935,7 @@

(inner) parseSource:
@@ -1097,7 +1097,7 @@

(inner) p
Source:
@@ -1285,7 +1285,7 @@

(inner) render
Source:
@@ -1499,7 +1499,7 @@

(inner) Source:
@@ -1651,6 +1651,168 @@

Returns:

+ +
+ + + +

(inner) templateToString(templateObject) → {String}

+ + + + + +
+

turns a template object back into a string

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
templateObject + + +Object + + + + +

template object obtained by using parseString or parse

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +String + + +
+
+ + +
+
    +
  • stringified template
  • +
+
+ + +
+ + + +
+ diff --git a/docs/code/site.js.html b/docs/code/site.js.html index 3089b1a..7435d44 100644 --- a/docs/code/site.js.html +++ b/docs/code/site.js.html @@ -24,7 +24,7 @@
@@ -39,17 +39,17 @@

site.js

-
const fs = require('fs');
-const path = require('path');
-const { promisify } = require('util');
+            
const fs = require('fs')
+const path = require('path')
+const { promisify } = require('util')
 
-const writeFile = promisify(fs.writeFile);
-const readdir = promisify(fs.readdir);
-const stat = promisify(fs.stat);
+const writeFile = promisify(fs.writeFile)
+const readdir = promisify(fs.readdir)
+const stat = promisify(fs.stat)
 
-const { parse, render, ensureDirectoryExists, merge, getTotalTimeOfDepends } = require('./util');
+const { parse, render, ensureDirectoryExists, merge, getTotalTimeOfDepends } = require('./util')
 
-const defaultPlugins = require('./defaultPlugins');
+const defaultPlugins = require('./defaultPlugins')
 
 class Site {
   /**
@@ -57,32 +57,42 @@ 

site.js

* @class Site * @param {Object} config - config options for the site */ - constructor(config={}) { - this.source = config.source || process.cwd(); - this.output = config.output || path.resolve(process.cwd(), 'site'); + constructor (config = {}) { + this.setup(config) + } + setup (config) { + // this will determine if the contents of the build are built to the output directory or in memory + this.inMemory = config.inMemory || false + this.source = config.source || process.cwd() + this.output = config.output || path.resolve(process.cwd(), 'site') + this.onBuild = typeof config.onBuild === 'function' ? config.onBuild : false // holds all template files and their parsed data - this.files = []; + this.files = [] // holds all the rendered files, used for when figuring out the time it took for a specific template or what templates need to be re-rendered if a file is changed. - this.rendered = []; + this.rendered = [] // this is what is used as a top level object for each file being rendered // files content will also be interpolated into this value - this.config = config || {}; - this.plugins = config.plugins ? merge(config.plugins, defaultPlugins) : defaultPlugins; + this.config = config || {} + this.plugins = config.plugins ? merge(config.plugins, defaultPlugins) : defaultPlugins } - async crawl(directory) { + async crawl (directory) { + const { output, source } = this // by default we want to crawl the source directory - if(!directory) directory = this.source; + if (!directory) directory = source - const files = await readdir(directory); + const files = await readdir(directory) - for(var i = 0; i < files.length; i++) { - const file = files[i]; - const stats = await stat(`${directory}/${file}`); - if(stats.isDirectory()) { - await this.crawl(`${directory}/${file}`); + for (var i = 0; i < files.length; i++) { + const file = files[i] + const stats = await stat(`${directory}/${file}`) + if (stats.isDirectory()) { + await this.crawl(`${directory}/${file}`) } - if(stats.isFile() && file.substr(file.lastIndexOf('.'), file.length) == '.sy') { - this.files.push(await parse(this.plugins, `${directory}/${file}`)); + if (stats.isFile() && file.substr(file.lastIndexOf('.'), file.length) === '.sy') { + const parsedFile = await parse(this.plugins, `${directory}/${file}`) + parsedFile.outputPath = path.resolve(output, `${parsedFile.name}.html`) + + this.files.push(parsedFile) } } } @@ -93,19 +103,17 @@

site.js

* @memberof Site * @return {Object} */ - get data() { - const { files, config } = this; - const d = {}; + get data () { + const { files, config } = this + const d = {} files.forEach((file) => { - const { options } = file; + const { options } = file - if(options.collection) { - if(!d[options.collection]) d[options.collection] = []; + if (!d[options.collection]) d[options.collection] = [] - d[options.collection].push(file); - } - }); - return Object.assign(d, config); + d[options.collection].push(file) + }) + return Object.assign(d, config) } /** * returns back pages and layouts as uncompiled templates @@ -113,27 +121,27 @@

site.js

* @memberof Site * @return {Object} - returns an object with the keys `layout` and `pages` that are hashmaps */ - categorize(files) { - const layouts = {}; - const pages = {}; + categorize (files) { + const layouts = {} + const pages = {} files.forEach((file) => { - const { type, filePath, name } = file; - switch(type) { + const { type, filePath, name } = file + switch (type) { case 'layout': // we are using the name of the file instead of the path for easier search and reference - layouts[name] = file; - break; + layouts[name] = file + break default: - pages[filePath] = file; - break; + pages[filePath] = file + break } - }); + }) return { layouts, pages - }; + } } /** * using the source directory, crawl all the necessary template files that will be ingested compiled @@ -141,46 +149,48 @@

site.js

* @memberof Site * @return {Promise} */ - async build() { + async build () { // TODO: in the future this should intelligently build files depending on what has been done // reset in case there were files previously set - this.files = []; - this.rendered = []; + this.files = [] + this.rendered = [] - const { output, files, config } = this; + const { output, files, inMemory, config } = this - await ensureDirectoryExists(output); - await this.crawl(); + await ensureDirectoryExists(output) + await this.crawl() - const { layouts, pages } = this.categorize(files); - const { data } = this; + const { layouts, pages } = this.categorize(files) + const { data } = this - for(var key of Object.keys(pages)) { - const page = pages[key]; - - // TODO: add how long the page took to render that could be used by the dev server as an overlay + for (var key of Object.keys(pages)) { + const page = pages[key] // If the user has provided a render method to handle different file types let them do that - if(config.render && typeof config.render === 'function') { - page.content = config.render(page.type, page.content); + if (config.render && typeof config.render === 'function') { + page.content = config.render(page.type, page.content) } - const options = await render(this.plugins, layouts, page, data); - - const outputFilePath = path.resolve(output, `${page.name}.html`); + const options = await render(this.plugins, layouts, page, data) this.rendered.push({ - filePath: outputFilePath, - time: getTotalTimeOfDepends(options), - depends: options - }); + filePath: page.outputPath, + data: options.data, + time: getTotalTimeOfDepends(options.depends), + depends: options.depends, + rendered: options.rendered + }) + + if (!inMemory) await writeFile(page.outputPath, options.rendered) + } - await writeFile(outputFilePath, options.rendered); + if (typeof this.onBuild === 'function') { + this.onBuild() } } } -module.exports = Site; +module.exports = Site
diff --git a/docs/code/template.js.html b/docs/code/template.js.html deleted file mode 100644 index e402848..0000000 --- a/docs/code/template.js.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - template.js - Documentation - - - - - - - - - - - - - - - - - -
- -

template.js

- - - - - - - -
-
-
const fs = require('fs');
-const path = require('path');
-
-class Template {
-  /**
-   * handles the state of a template and can render it
-   * @class Template
-   * @param  {String}    template    - a string containing the template
-   * @param  {String}    directory   - directory of where template if from
-   * @param  {String}    output      - the output directory to store assets
-   */
-  constructor(template, directory=process.cwd(), output=path.resolve(process.cwd(), 'site')) {
-    this.template = template;
-    this.directory = directory;
-    this.output = output;
-  }
-  /**
-   * takes an attributes object and turns it into a html string
-   * @method attributesToHTML
-   * @memberof Template
-   * @param  {Object} attributes - an object containing the html attributes
-   * @return {String} - html attributes string
-   */
-  attributesToHTML(attributes) {
-    if (!attributes) return '';
-    return Object.keys(attributes).filter((a) => {
-      return ['content'].indexOf(a) === -1;
-    }).map((a) => {
-      return `${a}="${attributes[a]}"`;
-    }).join(' ');
-  }
-  /**
-   * includes an asset or template from within a template
-   * @method include
-   * @memberof Template
-   * @param  {String} file       - relative path from file to asset
-   * @param  {Object} attributes - object with attribute keys that will turn into html
-   * @return {String}            - compiled template or asset tag
-   */
-  include(data, file, attributes) {
-    const { directory, output, attributesToHTML } = this;
-
-    const filePath = path.resolve(directory, file);
-    const extension = file.substr(file.lastIndexOf('.') + 1, file.length);
-    const fileName = file.substr(file.lastIndexOf('/') + 1, file.length);
-
-    switch (extension) {
-      case 'html':
-        return (new Template(fs.readFileSync(filePath).toString(), directory, output)).render(data);
-      case 'css':
-        fs.createReadStream(filePath).pipe(fs.createWriteStream(path.resolve(output, fileName)));
-        return `<link rel="stylesheet" href="./${fileName}" ${attributesToHTML(attributes)}>`;
-      case 'png':
-      case 'jpeg':
-      case 'gif':
-      case 'jpg':
-      case 'svg':
-        fs.createReadStream(filePath).pipe(fs.createWriteStream(path.resolve(output, fileName)));
-        return `<img src="./${fileName}" ${attributesToHTML(attributes)}>`;
-    }
-  }
-  /**
-   * renders the template to string
-   * @method render
-   * @memberof Template
-   * @param  {Object}    data        - object that contains state for template
-   * @return {string} - compiled template
-   */
-  render(data) {
-    const { template, include } = this;
-
-    data.include = include.bind(this, data);
-
-    const templ = template.replace(/[\r\t\n]/g, ' ');
-    const re = /{{[\s]*(.+?)[\s]*}}/g;
-    const reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
-
-    let code = 'var r=[];\n';
-    let cursor = 0;
-    let match;
-
-    function add(line, js) {
-      js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
-        (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
-      return add;
-    }
-    while (match = re.exec(templ)) {
-      add(templ.slice(cursor, match.index))(match[1], true);
-      cursor = match.index + match[0].length;
-    }
-    add(templ.substr(cursor, templ.length - cursor));
-    code += 'return r.join("");';
-    return new Function(`
-      with(this) {
-        ${code.replace(/[\r\t\n]/g, '')}
-      }
-    `).bind(data)();
-  }
-}
-
-module.exports = Template;
-
-
-
- - - - -
- -
- - - - - - - diff --git a/docs/code/util.js.html b/docs/code/util.js.html index 6d14d45..5ab9db2 100644 --- a/docs/code/util.js.html +++ b/docs/code/util.js.html @@ -24,7 +24,7 @@
@@ -43,31 +43,31 @@

util.js

* @module lib/util */ -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') -const stat = promisify(fs.stat); -const mkdir = promisify(fs.mkdir); -const writeFile = promisify(fs.writeFile); -const readFile = promisify(fs.readFile); -const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat) +const mkdir = promisify(fs.mkdir) +const writeFile = promisify(fs.writeFile) +const readFile = promisify(fs.readFile) +const readdir = promisify(fs.readdir) /** * ensures the given path exists, if not recursively generates folder leading to the paths * @method ensureDirectoryExists * @param {String} directory - the given path */ -async function ensureDirectoryExists(directory) { - const parts = directory.split('/').slice(1); - let currentDirectory = ''; +async function ensureDirectoryExists (directory) { + const parts = directory.split('/').slice(1) + let currentDirectory = '' while (parts.length > 0) { - currentDirectory += `/${parts.shift()}`; + currentDirectory += `/${parts.shift()}` try { - await stat(currentDirectory); + await stat(currentDirectory) } catch (ex) { - await mkdir(currentDirectory); + await mkdir(currentDirectory) } } } @@ -78,20 +78,20 @@

util.js

* @param {String} source - path to source * @param {String} destination - path to destination */ -async function copyDirectory(source, destination) { - const stats = await stat(source); +async function copyDirectory (source, destination) { + const stats = await stat(source) if (!stats.isFile() && stats.isDirectory()) { - await ensureDirectoryExists(destination); + await ensureDirectoryExists(destination) - const files = await readdir(source); + const files = await readdir(source) - for(var i = 0; i < files.length; i++) { - const childItemName = files[i]; - await copyDirectory(path.join(source, childItemName), path.join(destination, childItemName)); + for (var i = 0; i < files.length; i++) { + const childItemName = files[i] + await copyDirectory(path.join(source, childItemName), path.join(destination, childItemName)) } } else { - await writeFile(destination, await readFile(source)); + await writeFile(destination, await readFile(source)) } } @@ -101,27 +101,47 @@

util.js

* @param {String} directory - path of directory * @return {Object} - config object */ -async function getConfig(directory) { +async function getConfig (directory) { try { - await stat(`${directory}/.sweeney`); + await stat(`${directory}/.sweeney`) - const config = require(`${directory}/.sweeney`); + const config = require(`${directory}/.sweeney`) switch (typeof config) { case 'object': - return config; + return config case 'function': - return config(); + return config() } } catch (ex) { // propogate message to the user, this is something with the config if (ex.message.indexOf('no such file or directory') === -1) { - throw ex; + throw ex } - return {}; + return {} } } +function escapeRegexValues (string) { + return string.replace(/[-[\]{}()*+!<=:?.\\^$|#\s,]/g, '\\$&') +} + +/** + * turns a template object back into a string + * @param {Object} templateObject - template object obtained by using parseString or parse + * @return {String} - stringified template + */ +function templateToString (templateObject) { + const { options, content, type } = templateObject + + return ` +--- +${JSON.stringify(Object.assign(options, { type }), null, 4)} +--- +${content} +`.trim() +} + /** * parses a template string for any options or content it contains * @method parseString @@ -129,41 +149,46 @@

util.js

* @param {String} content - Template string * @return {Object} - parsed template output */ -async function parseString(plugins, filePath, content) { - const config = {}; +async function parseString (plugins, filePath, content) { + const config = {} // this is the name of the file without the extension, used for internal use or during the output process - config.name = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length - 3); + config.name = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length - 3) // let's run through the plugins - const pluginNames = Object.keys(plugins); - for(const name of pluginNames) { - const plugin = plugins[name]; - - const output = await plugin.parse(filePath, content); - if(output) { - if(output.content !== content) content = output.content; // update the content with the augmented one - if(output.found.length > 0) config[name] = output.found; + const pluginNames = Object.keys(plugins) + for (const name of pluginNames) { + const plugin = plugins[name] + if (!plugin.parse) continue + + const output = await plugin.parse(filePath, content) + if (output) { + if (output.content !== content) content = output.content // update the content with the augmented one + if (output.found.length > 0) config[name] = output.found } } if (/^---\n/.test(content)) { - var end = content.search(/\n---\n/); - if (end != -1) { - const parsed = JSON.parse(content.slice(4, end + 1)); + var end = content.search(/\n---\n/) + if (end !== -1) { + const parsed = JSON.parse(content.slice(4, end + 1)) // this is the top level attribute in the render function that will allow users to do things like // {{ posts.forEach((post) => {...}) }} _In this case posts will be an array_ - config.collection = parsed.collection || 'page'; + config.collection = parsed.collection || 'page' // type is the value that the parser will look at to decide what to do with that file - config.type = parsed.type || 'html'; - config.options = parsed; - config.content = content.slice(end + 5); + config.type = parsed.type || 'html' + config.options = parsed + config.content = content.slice(end + 5) - if (parsed.layout) config.layout = parsed.layout; + if (parsed.layout) config.layout = parsed.layout } + } else { + config.content = content + config.type = 'html' + config.collection = 'page' } - return config; + return config } /** @@ -172,17 +197,16 @@

util.js

* @param {String} content - file contents that could potentially contain options * @return {Object} - with the attributes options and content */ -async function parse(plugins, filePath) { - const content = await readFile(filePath, 'utf8'); +async function parse (plugins, filePath) { + const content = await readFile(filePath, 'utf8') const config = { filePath, options: {}, content: content - }; - + } - return Object.assign(config, await parseString(plugins, filePath, content)); + return Object.assign(config, await parseString(plugins, filePath, content)) } /** @@ -192,25 +216,25 @@

util.js

* @param {Number} ms - time in milleseconds * @return {String} - human readable string */ -const s = 1000; -const m = s * 60; -const h = m * 60; -const d = h * 24; +const s = 1000 +const m = s * 60 +const h = m * 60 +const d = h * 24 -function ms(ms) { +function ms (ms) { if (ms >= d) { - return `${Math.floor(ms / d)}d`; + return `${Math.floor(ms / d)}d` } if (ms >= h) { - return `${Math.floor(ms / h)}h`; + return `${Math.floor(ms / h)}h` } if (ms >= m) { - return `${Math.floor(ms / m)}m`; + return `${Math.floor(ms / m)}m` } if (ms >= s) { - return `${Math.floor(ms / s)}s`; + return `${Math.floor(ms / s)}s` } - return ms.toFixed(4).replace(/\.0000$/, '') + 'ms'; + return ms.toFixed(4).replace(/\.0000$/, '') + 'ms' } /** @@ -221,97 +245,111 @@

util.js

* @param {Object} additional - additional data that needs to be merged into template data * @return {String} - rendered template to string */ -async function render(plugins, templates, template, additional = {}) { - const start = process.hrtime(); +async function render (plugins, templates, template, additional = {}) { + const start = process.hrtime() - const { filePath } = template; - let { content = '' } = template; + const { filePath } = template + let { content = '' } = template // combine the data from the template and whatever is passed in - const data = merge(additional, template); - if(!content) return; + const data = merge(additional, template) + if (!content) return try { // let's run through rendering any plugin data that was parsed out - const pluginNames = Object.keys(plugins); - const tempDepends = []; - for(var p = 0; p < pluginNames.length; p++) { - const name = pluginNames[p]; - const plugin = plugins[name]; - - if(data[name] && data[name].length > 0) { - for(const found of data[name]) { - const output = await plugin.render(plugins, filePath, content, templates, data, found); - - if(output.content) content = output.content; - if(output.depends) tempDepends.push(output.depends); + const pluginNames = Object.keys(plugins) + const tempDepends = [] + for (var p = 0; p < pluginNames.length; p++) { + const name = pluginNames[p] + const plugin = plugins[name] + + if (data[name] && data[name].length > 0) { + for (const found of data[name]) { + if (!plugin.render) continue + + const output = await plugin.render(plugins, filePath, content, templates, data, found) + + if (output.content) content = output.content + if (output.depends) tempDepends.push(output.depends) } } } - if(tempDepends.length > 0) { - data.depends = [].concat.apply([], tempDepends); + if (tempDepends.length > 0) { + data.depends = [].concat.apply([], tempDepends) } - const templ = content.replace(/[\r\t\n]/g, ' '); - const re = /{{[\s]*(.+?)[\s]*}}/g; - const reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g; + const templ = content.replace(/[\r\t\n]/g, ' ') + const re = /{{[\s]*(.+?)[\s]*}}/g + + let code = 'var r=[];\n' + let cursor = 0 + let match = '' - let code = 'var r=[];\n'; - let cursor = 0; - let match; + function add (line, js) { // eslint-disable-line no-inner-declarations + const reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g - function add(line, js) { - js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : - (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); - return add; + js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') + : (code += line !== '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '') + return add } - while (match = re.exec(templ)) { - add(templ.slice(cursor, match.index))(match[1], true); - cursor = match.index + match[0].length; + + while ((match = re.exec(templ)) !== null) { + add(templ.slice(cursor, match.index))(match[1], true) + cursor = match.index + match[0].length } - add(templ.substr(cursor, templ.length - cursor)); - code += 'return r.join("");'; + add(templ.substr(cursor, templ.length - cursor)) + code += 'return r.join("");' + /* eslint-disable no-new-func */ const rendered = new Function(` with(this) { ${code.replace(/[\r\t\n]/g, '')} } - `).bind(data)(); + `).bind(data)() + /* eslint-enable no-new-func */ // if this template has a layout file we must render this then the layout file - const layout = template.layout && templates[template.layout]; + const layout = template.layout && templates[template.layout] // why are we doing this, well we pass around data a fairbit and we want to snapshot the state of the depends field at this point - const depends = data.depends ? JSON.parse(JSON.stringify(data.depends)) : []; + const depends = data.depends ? JSON.parse(JSON.stringify(data.depends)) : [] + + const safeData = {} + Object.keys(data).forEach((k) => { + if (k !== 'depends' && k !== 'layout') { + safeData[k] = data[k] + } + }) if (layout) { // we have to delete the old layout - delete data['layout']; + delete data['layout'] const output = await render(plugins, templates, layout, Object.assign({ - child: rendered, - depends: [] // pass in an empty data to stop from creating duplicate data - }, data)); + child: rendered + }, safeData)) return { + data: JSON.parse(JSON.stringify(safeData)), filePath, depends: output, rendered: output.rendered, - time: process.hrtime(start)[1]/1000000 - }; + time: process.hrtime(start)[1] / 1000000 + } } else { return { + data: JSON.parse(JSON.stringify(safeData)), filePath, rendered, depends, // this is if any plugin has added dependency information to the template - time: process.hrtime(start)[1]/1000000 - }; + time: process.hrtime(start)[1] / 1000000 + } } } catch (ex) { throw new Error(JSON.stringify({ error: `Error building template ${filePath}`, content: content, stack: ex.stack - })); + })) } } @@ -322,24 +360,24 @@

util.js

* @param {Object|Array} source - an array or object, must be the same type as the other value being passed * @return {Object|Array} - an array or object, depending on what has been passed in */ -function merge(target, source) { - if (typeof target == 'object' && typeof source == 'object') { +function merge (target, source) { + if (typeof target === 'object' && typeof source === 'object') { for (const key in source) { if (source[key] === null && (target[key] === undefined || target[key] === null)) { - target[key] = null; + target[key] = null } else if (source[key] instanceof Array) { - if (!target[key]) target[key] = []; - //concatenate arrays - target[key] = target[key].concat(source[key]); - } else if (typeof source[key] == 'object') { - if (!target[key]) target[key] = {}; - merge(target[key], source[key]); + if (!target[key]) target[key] = [] + // concatenate arrays + target[key] = target[key].concat(source[key]) + } else if (typeof source[key] === 'object') { + if (!target[key]) target[key] = {} + merge(target[key], source[key]) } else { - target[key] = source[key]; + target[key] = source[key] } } } - return target; + return target } /** @@ -347,21 +385,21 @@

util.js

* @param {Object} item - rendered item output by render method * @return {Number} - time in milleseconds */ -function getTotalTimeOfDepends(item) { - let time = 0; +function getTotalTimeOfDepends (item) { + let time = 0 - if(Array.isArray(item)) { - time += item.map((i) => getTotalTimeOfDepends(i)).reduce((a, b) => a + b, 0); + if (Array.isArray(item)) { + time += item.map((i) => getTotalTimeOfDepends(i)).reduce((a, b) => a + b, 0) } - if(!Array.isArray(item) && typeof item === 'object') { - time += item.time; + if (!Array.isArray(item) && typeof item === 'object') { + time += item.time } // recursively find the depends values - if(item.depends) time += getTotalTimeOfDepends(item.depends); + if (item.depends) time += getTotalTimeOfDepends(item.depends) - return time; + return time } /** @@ -370,20 +408,20 @@

util.js

* @param {Number} level - the level of the tree that the element is a part of (by default is 0) * @return {String} - ascii representation of render tree for the given item */ -function renderSubDepends(item, level=0) { - let output = ''; +function renderSubDepends (item, level = 0) { + let output = '' - if(Array.isArray(item)) { - output += item.map((i) => renderSubDepends(i, level)).join(''); + if (Array.isArray(item)) { + output += item.map((i) => renderSubDepends(i, level)).join('') } - if(!Array.isArray(item) && typeof item === 'object') { - output += '\n' + `${level === 0 ? '' : ' '.repeat(level * 2) + '-'} ${item.filePath} [${ms(item.time)}]`; + if (!Array.isArray(item) && typeof item === 'object') { + output += '\n' + `${level === 0 ? '' : ' '.repeat(level * 2) + '-'} ${item.filePath} [${ms(item.time)}]` } - if(item.depends) output += renderSubDepends(item.depends, level + 1); + if (item.depends) output += renderSubDepends(item.depends, level + 1) - return output ; + return output } module.exports = { @@ -393,11 +431,13 @@

util.js

render, ms, getConfig, + escapeRegexValues, ensureDirectoryExists, copyDirectory, renderSubDepends, + templateToString, getTotalTimeOfDepends -}; +}
diff --git a/docs/index.html b/docs/index.html index 8f5cfd7..b47ce4b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -203,6 +203,33 @@

A static site generator that cuts the way you wa

+ + diff --git a/example/layouts/default.sy b/example/layouts/default.sy index b679e1c..344ddae 100644 --- a/example/layouts/default.sy +++ b/example/layouts/default.sy @@ -12,7 +12,7 @@ - {{ options.title || site.title }} + {{-- editable options.title || site.title string --}} {{-- includes ../site.css --}} diff --git a/lib/defaultPlugins.js b/lib/defaultPlugins.js index 68b97e2..06bbf57 100644 --- a/lib/defaultPlugins.js +++ b/lib/defaultPlugins.js @@ -1,69 +1,111 @@ -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') -const { render, parse } = require('./util'); +const { render, parse } = require('./util') -const readFile = promisify(fs.readFile); +const readFile = promisify(fs.readFile) + +module.exports.editable = { + parse: async function (filePath, content) { + const reg = /{{-- editable (.+?) (string|number) --}}/g + + if (content.match(reg)) { + const found = [] + let block + while ((block = reg.exec(content)) != null) { + const variableName = block[1] + const type = block[2] + + // the classic example value1 || value2 + if (variableName.indexOf('||') > -1) { + const choices = variableName.split('||') + + choices.forEach((choice) => { + found.push({ + filePath, + variableName: choice.trim(), + type + }) + }) + } else { + found.push({ + filePath, + variableName, + type + }) + } + + // we want the parser to simply inject the required values back here, so just convert it to a basic variable template + content = content.replace(`{{-- editable ${variableName} ${type} --}}`, `{{ ${variableName} }}`) + } + return { + content, + found + } + } + return false + } +} module.exports.includes = { - parse: async function(filePath, content) { - const reg = /{{-- includes (.+?) --}}/g; + parse: async function (filePath, content) { + const reg = /{{-- includes (.+?) --}}/g - if(content.match(reg)) { - const found = []; - let block; + if (content.match(reg)) { + const found = [] + let block while ((block = reg.exec(content)) != null) { - const oldArgument = block[1]; - const newArgument = path.resolve(path.dirname(filePath), oldArgument); + const oldArgument = block[1] + const newArgument = path.resolve(path.dirname(filePath), oldArgument) - content = content.replace(`{{-- includes ${oldArgument} --}}`, `{{-- includes ${newArgument} --}}`); - found.push(newArgument); + content = content.replace(`{{-- includes ${oldArgument} --}}`, `{{-- includes ${newArgument} --}}`) + found.push(newArgument) } return { content, found - }; + } } - return false; + return false }, - render: async function(plugins, filePath, content, templates, data, found) { - const start = process.hrtime(); - const ext = found.substring(found.lastIndexOf('.') + 1, found.length); - const name = found.substring(found.lastIndexOf('/') + 1, found.length - 3); - const _content = await readFile(found, 'utf8'); - const depends = []; - - if(ext === 'sy') { + render: async function (plugins, filePath, content, templates, data, found) { + const start = process.hrtime() + const ext = found.substring(found.lastIndexOf('.') + 1, found.length) + const name = found.substring(found.lastIndexOf('/') + 1, found.length - 3) + const _content = await readFile(found, 'utf8') + const depends = [] + + if (ext === 'sy') { const passedData = Object.assign(data, { layout: '', depends: [], includes: [] - }); + }) // if the template isn't registered (ie it is somewhere else on disk and not in the site directory) go and parse that file - const _template = templates[name] || await parse(plugins, found); - const output = await render(plugins, templates, _template, passedData); + const _template = templates[name] || await parse(plugins, found) + const output = await render(plugins, templates, _template, passedData) // ensure this template gets added to the dependency tree - depends.push(output); + depends.push(output) - content = content.replace(`{{-- includes ${found} --}}`, output.rendered); + content = content.replace(`{{-- includes ${found} --}}`, output.rendered) } - if(ext === 'css') { - content = content.replace(`{{-- includes ${found} --}}`, ``); + if (ext === 'css') { + content = content.replace(`{{-- includes ${found} --}}`, ``) // ensure this content gets added to the dependency tree depends.push({ filePath: found, - time: process.hrtime(start)[1]/1000000 - }); + time: process.hrtime(start)[1] / 1000000 + }) } return { depends, content - }; + } } -}; +} diff --git a/lib/mimes.js b/lib/mimes.js index a5d4b3b..95a22ae 100644 --- a/lib/mimes.js +++ b/lib/mimes.js @@ -1092,4 +1092,4 @@ module.exports = { movie: 'video/x-sgi-movie', smv: 'video/x-smv', ice: 'x-conference/x-cooltalk' -}; +} diff --git a/lib/serve/serve.css b/lib/serve/serve.css new file mode 100644 index 0000000..a6dc06a --- /dev/null +++ b/lib/serve/serve.css @@ -0,0 +1,77 @@ +.editable-body { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: wrap; +} +.editable-body__sidebar { + width: 10%; + z-index: 1000; +} +.editable-body__page { + width: 90%; + position: relative; + margin: 0 auto; +} + +.editable-container { + padding: 10px; + text-align: center; + border-right: 3px solid #e8e8e8; + background: #fff; + height: 100%; +} +.editable-container__save { + border: 1px solid #dedede; + background: #ffff; + width: 100%; + padding: 5px 0; + margin: 3px 0; +} +.editable-container__item { + text-align: left; +} +.editable-container__item-header { + margin: 0; +} +.editable-container__item-list { + list-style:none; + margin: 0; + padding: 0; +} +.editable-container__item-list-item { + margin: 10px; +} +.editable-container__item-list-item__label { + display: block; +} +.editable-container__item-list-item__input { + width: 90%; + height: 25px; + padding: 3px; + margin: 3px 0; +} + +.navigation-controls { + margin-top: 20px; + width: 100%; + + display: flex; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; +} +.navigation-controls__item:first-child { + border-left: 1px solid #dedede; +} +.navigation-controls__item { + display: inline-block; + flex-grow: 1; + width: 25px; + height: 25px; + border-top: 1px solid #dedede; + border-bottom: 1px solid #dedede; + border-right: 1px solid #dedede; + padding: 10px; +} diff --git a/lib/serve/serve.js b/lib/serve/serve.js new file mode 100644 index 0000000..7346b1b --- /dev/null +++ b/lib/serve/serve.js @@ -0,0 +1,301 @@ +const fs = require('fs') +const path = require('path') +const http = require('http') +const { promisify } = require('util') +const formidable = require('formidable') + +const writeFile = promisify(fs.writeFile) +const readFile = promisify(fs.readFile) + +const mimes = require('../mimes') +const { ms, merge, escapeRegexValues, getConfig, templateToString } = require('../util') + +function makeSearchable (object, _search, parent) { + const search = _search || {} + + Object.keys(object).forEach((key) => { + if (typeof object[key] === 'object' && !Array.isArray(object[key])) { + makeSearchable(object[key], search, key) + } else if (Array.isArray(object[key])) { + + } else { + if (parent) { + search[`${parent}.${key}`] = object[key] + } else { + search[key] = object[key] + } + } + }) + + return search +} + +function getEditable (page) { + let editable = {} + + if (page.data && page.data.editable) { + const searchAbleObject = makeSearchable(page.data) + + page.data.editable.forEach((_editable) => { + if (!editable[_editable.filePath]) editable[_editable.filePath] = {} + editable[_editable.filePath][_editable.variableName] = { + type: _editable.type, + value: searchAbleObject[_editable.variableName] + } + }) + } + + if (page.depends) { + editable = merge(editable, getEditable(page.depends)) + } + + return editable +} + +function set (obj, path, value) { + const pList = path.split('.') + const key = pList.pop() + const pointer = pList.reduce((accumulator, currentValue) => { + if (accumulator[currentValue] === undefined) accumulator[currentValue] = {} + return accumulator[currentValue] + }, obj) + pointer[key] = value + return obj +} + +function get (obj, path) { + const pList = path.split('.') + const key = pList.pop() + const pointer = pList.reduce((accumulator, currentValue) => { + if (accumulator[currentValue] === undefined) accumulator[currentValue] = {} + return accumulator[currentValue] + }, obj) + return pointer[key] +} + +module.exports = async function ({ port, watch, site }) { + const styles = await readFile(path.resolve(__dirname, 'serve.css')) + let build = new Date() + + site.onBuild = function () { + build = new Date() + } + + return new Promise(function (resolve, reject) { + const server = http.createServer(async (req, res) => { + // TODO: clean this up + if (req.url === '/__api/update') { + res.statusCode = 200 + return res.end(build.toString()) + } + if (req.url === '/_api/update/config') { + var form = new formidable.IncomingForm() + return form.parse(req, async (error, fields, files) => { + if (error) { + res.statusCode = 500 + res.end(` + + + + Sweeney + + +
+ Something went wrong: + +
+                    ${error.stack}
+                  
+
+ + `) + } + + // We don't have to trigger a rebuild, since we are watching it should rebuild itself + Object.keys(fields).forEach(async (key) => { + if (key === 'filePath' || key === 'parentFilePath') return + + // only set the top level data on the config itself, everything else should be in the files + if (key.indexOf('options') === -1) { + const configPath = path.resolve(site.source, '.sweeney') + let rawConfig = await readFile(configPath, 'utf8') + // we are only looking for the last value in the key string + const finalKey = key.substring(key.lastIndexOf('.') + 1, key.length) + const previousValue = escapeRegexValues(get(site.config, key)) + const reg = new RegExp(`((['|"]*)${finalKey}(['|"]*)\\s*:\\s*(['|"]*)${previousValue}(['|"]*))`) + // TODO: we need to make sure if this is a number we don't add quotes + rawConfig = rawConfig.replace(reg, function (value) { + return `'${finalKey}':'${fields[key]}'` + }) + await writeFile(configPath, rawConfig, 'utf8') + + delete require.cache[require.resolve(configPath)] + + site.config = await getConfig(site.source) + } else { + site.files.forEach(async (f) => { + if (f.filePath === fields['parentFilePath']) { + set(f, key, fields[key]) + const newContent = templateToString(f) + await writeFile(fields['parentFilePath'], newContent, 'utf8') + } + }) + } + }) + + res.writeHead(302, { + 'Location': req.headers['referer'] + }) + res.end() + }) + } + const file = (req.url === '/' ? '/index.html' : req.url) || '/index.html' + const ext = file.substr(file.lastIndexOf('.') + 1, file.length) + + try { + // removing the leading / from the file name + const fullPath = path.resolve(site.output, file.substr(1, file.length)) + // if we have a rendered file for what is being requested, use the rendered file + const parsedFileName = Object.keys(site.rendered).filter((name) => site.rendered[name].filePath === fullPath)[0] + + const parsedFile = parsedFileName ? site.rendered[parsedFileName] : null + + let contents = parsedFile ? parsedFile.rendered : await readFile(fullPath, 'utf8') + // inject javascript into the page to refresh it in the case that a new build occurs + if (ext === 'html' && watch !== undefined) { + const editable = getEditable(parsedFile) + + contents = contents.replace('', ``) + } + + res.writeHead(200, { + 'Content-Type': mimes[ext] + }) + res.end(contents) + } catch (ex) { + // TODO: make this more user friendly (find the issue and offer suggestions) + res.writeHead(500, { + 'Content-Type': mimes['json'] + }) + res.end(JSON.stringify({ + error: ex.stack + })) + } + }) + server.listen(port, (error) => error ? reject(error) : resolve(server)) + }) +} diff --git a/lib/site.js b/lib/site.js index 2c8c2e6..52a99d9 100644 --- a/lib/site.js +++ b/lib/site.js @@ -1,14 +1,14 @@ -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') -const writeFile = promisify(fs.writeFile); -const readdir = promisify(fs.readdir); -const stat = promisify(fs.stat); +const writeFile = promisify(fs.writeFile) +const readdir = promisify(fs.readdir) +const stat = promisify(fs.stat) -const { parse, render, ensureDirectoryExists, merge, getTotalTimeOfDepends } = require('./util'); +const { parse, render, ensureDirectoryExists, merge, getTotalTimeOfDepends } = require('./util') -const defaultPlugins = require('./defaultPlugins'); +const defaultPlugins = require('./defaultPlugins') class Site { /** @@ -16,32 +16,42 @@ class Site { * @class Site * @param {Object} config - config options for the site */ - constructor(config={}) { - this.source = config.source || process.cwd(); - this.output = config.output || path.resolve(process.cwd(), 'site'); + constructor (config = {}) { + this.setup(config) + } + setup (config) { + // this will determine if the contents of the build are built to the output directory or in memory + this.inMemory = config.inMemory || false + this.source = config.source || process.cwd() + this.output = config.output || path.resolve(process.cwd(), 'site') + this.onBuild = typeof config.onBuild === 'function' ? config.onBuild : false // holds all template files and their parsed data - this.files = []; + this.files = [] // holds all the rendered files, used for when figuring out the time it took for a specific template or what templates need to be re-rendered if a file is changed. - this.rendered = []; + this.rendered = [] // this is what is used as a top level object for each file being rendered // files content will also be interpolated into this value - this.config = config || {}; - this.plugins = config.plugins ? merge(config.plugins, defaultPlugins) : defaultPlugins; + this.config = config || {} + this.plugins = config.plugins ? merge(config.plugins, defaultPlugins) : defaultPlugins } - async crawl(directory) { + async crawl (directory) { + const { output, source } = this // by default we want to crawl the source directory - if(!directory) directory = this.source; + if (!directory) directory = source - const files = await readdir(directory); + const files = await readdir(directory) - for(var i = 0; i < files.length; i++) { - const file = files[i]; - const stats = await stat(`${directory}/${file}`); - if(stats.isDirectory()) { - await this.crawl(`${directory}/${file}`); + for (var i = 0; i < files.length; i++) { + const file = files[i] + const stats = await stat(`${directory}/${file}`) + if (stats.isDirectory()) { + await this.crawl(`${directory}/${file}`) } - if(stats.isFile() && file.substr(file.lastIndexOf('.'), file.length) == '.sy') { - this.files.push(await parse(this.plugins, `${directory}/${file}`)); + if (stats.isFile() && file.substr(file.lastIndexOf('.'), file.length) === '.sy') { + const parsedFile = await parse(this.plugins, `${directory}/${file}`) + parsedFile.outputPath = path.resolve(output, `${parsedFile.name}.html`) + + this.files.push(parsedFile) } } } @@ -52,19 +62,17 @@ class Site { * @memberof Site * @return {Object} */ - get data() { - const { files, config } = this; - const d = {}; + get data () { + const { files, config } = this + const d = {} files.forEach((file) => { - const { options } = file; + const { options } = file - if(options.collection) { - if(!d[options.collection]) d[options.collection] = []; + if (!d[options.collection]) d[options.collection] = [] - d[options.collection].push(file); - } - }); - return Object.assign(d, config); + d[options.collection].push(file) + }) + return Object.assign(d, config) } /** * returns back pages and layouts as uncompiled templates @@ -72,27 +80,27 @@ class Site { * @memberof Site * @return {Object} - returns an object with the keys `layout` and `pages` that are hashmaps */ - categorize(files) { - const layouts = {}; - const pages = {}; + categorize (files) { + const layouts = {} + const pages = {} files.forEach((file) => { - const { type, filePath, name } = file; - switch(type) { + const { type, filePath, name } = file + switch (type) { case 'layout': // we are using the name of the file instead of the path for easier search and reference - layouts[name] = file; - break; + layouts[name] = file + break default: - pages[filePath] = file; - break; + pages[filePath] = file + break } - }); + }) return { layouts, pages - }; + } } /** * using the source directory, crawl all the necessary template files that will be ingested compiled @@ -100,43 +108,45 @@ class Site { * @memberof Site * @return {Promise} */ - async build() { + async build () { // TODO: in the future this should intelligently build files depending on what has been done // reset in case there were files previously set - this.files = []; - this.rendered = []; + this.files = [] + this.rendered = [] - const { output, files, config } = this; + const { output, files, inMemory, config } = this - await ensureDirectoryExists(output); - await this.crawl(); + await ensureDirectoryExists(output) + await this.crawl() - const { layouts, pages } = this.categorize(files); - const { data } = this; + const { layouts, pages } = this.categorize(files) + const { data } = this - for(var key of Object.keys(pages)) { - const page = pages[key]; - - // TODO: add how long the page took to render that could be used by the dev server as an overlay + for (var key of Object.keys(pages)) { + const page = pages[key] // If the user has provided a render method to handle different file types let them do that - if(config.render && typeof config.render === 'function') { - page.content = config.render(page.type, page.content); + if (config.render && typeof config.render === 'function') { + page.content = config.render(page.type, page.content) } - const options = await render(this.plugins, layouts, page, data); - - const outputFilePath = path.resolve(output, `${page.name}.html`); + const options = await render(this.plugins, layouts, page, data) this.rendered.push({ - filePath: outputFilePath, - time: getTotalTimeOfDepends(options), - depends: options - }); + filePath: page.outputPath, + data: options.data, + time: getTotalTimeOfDepends(options.depends), + depends: options.depends, + rendered: options.rendered + }) + + if (!inMemory) await writeFile(page.outputPath, options.rendered) + } - await writeFile(outputFilePath, options.rendered); + if (typeof this.onBuild === 'function') { + this.onBuild() } } } -module.exports = Site; +module.exports = Site diff --git a/lib/util.js b/lib/util.js index 5463826..7d03893 100644 --- a/lib/util.js +++ b/lib/util.js @@ -2,31 +2,31 @@ * @module lib/util */ -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') -const stat = promisify(fs.stat); -const mkdir = promisify(fs.mkdir); -const writeFile = promisify(fs.writeFile); -const readFile = promisify(fs.readFile); -const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat) +const mkdir = promisify(fs.mkdir) +const writeFile = promisify(fs.writeFile) +const readFile = promisify(fs.readFile) +const readdir = promisify(fs.readdir) /** * ensures the given path exists, if not recursively generates folder leading to the paths * @method ensureDirectoryExists * @param {String} directory - the given path */ -async function ensureDirectoryExists(directory) { - const parts = directory.split('/').slice(1); - let currentDirectory = ''; +async function ensureDirectoryExists (directory) { + const parts = directory.split('/').slice(1) + let currentDirectory = '' while (parts.length > 0) { - currentDirectory += `/${parts.shift()}`; + currentDirectory += `/${parts.shift()}` try { - await stat(currentDirectory); + await stat(currentDirectory) } catch (ex) { - await mkdir(currentDirectory); + await mkdir(currentDirectory) } } } @@ -37,20 +37,20 @@ async function ensureDirectoryExists(directory) { * @param {String} source - path to source * @param {String} destination - path to destination */ -async function copyDirectory(source, destination) { - const stats = await stat(source); +async function copyDirectory (source, destination) { + const stats = await stat(source) if (!stats.isFile() && stats.isDirectory()) { - await ensureDirectoryExists(destination); + await ensureDirectoryExists(destination) - const files = await readdir(source); + const files = await readdir(source) - for(var i = 0; i < files.length; i++) { - const childItemName = files[i]; - await copyDirectory(path.join(source, childItemName), path.join(destination, childItemName)); + for (var i = 0; i < files.length; i++) { + const childItemName = files[i] + await copyDirectory(path.join(source, childItemName), path.join(destination, childItemName)) } } else { - await writeFile(destination, await readFile(source)); + await writeFile(destination, await readFile(source)) } } @@ -60,27 +60,47 @@ async function copyDirectory(source, destination) { * @param {String} directory - path of directory * @return {Object} - config object */ -async function getConfig(directory) { +async function getConfig (directory) { try { - await stat(`${directory}/.sweeney`); + await stat(`${directory}/.sweeney`) - const config = require(`${directory}/.sweeney`); + const config = require(`${directory}/.sweeney`) switch (typeof config) { case 'object': - return config; + return config case 'function': - return config(); + return config() } } catch (ex) { // propogate message to the user, this is something with the config if (ex.message.indexOf('no such file or directory') === -1) { - throw ex; + throw ex } - return {}; + return {} } } +function escapeRegexValues (string) { + return string.replace(/[-[\]{}()*+!<=:?.\\^$|#\s,]/g, '\\$&') +} + +/** + * turns a template object back into a string + * @param {Object} templateObject - template object obtained by using parseString or parse + * @return {String} - stringified template + */ +function templateToString (templateObject) { + const { options, content, type } = templateObject + + return ` +--- +${JSON.stringify(Object.assign(options, { type }), null, 4)} +--- +${content} +`.trim() +} + /** * parses a template string for any options or content it contains * @method parseString @@ -88,41 +108,46 @@ async function getConfig(directory) { * @param {String} content - Template string * @return {Object} - parsed template output */ -async function parseString(plugins, filePath, content) { - const config = {}; +async function parseString (plugins, filePath, content) { + const config = {} // this is the name of the file without the extension, used for internal use or during the output process - config.name = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length - 3); + config.name = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length - 3) // let's run through the plugins - const pluginNames = Object.keys(plugins); - for(const name of pluginNames) { - const plugin = plugins[name]; - - const output = await plugin.parse(filePath, content); - if(output) { - if(output.content !== content) content = output.content; // update the content with the augmented one - if(output.found.length > 0) config[name] = output.found; + const pluginNames = Object.keys(plugins) + for (const name of pluginNames) { + const plugin = plugins[name] + if (!plugin.parse) continue + + const output = await plugin.parse(filePath, content) + if (output) { + if (output.content !== content) content = output.content // update the content with the augmented one + if (output.found.length > 0) config[name] = output.found } } if (/^---\n/.test(content)) { - var end = content.search(/\n---\n/); - if (end != -1) { - const parsed = JSON.parse(content.slice(4, end + 1)); + var end = content.search(/\n---\n/) + if (end !== -1) { + const parsed = JSON.parse(content.slice(4, end + 1)) // this is the top level attribute in the render function that will allow users to do things like // {{ posts.forEach((post) => {...}) }} _In this case posts will be an array_ - config.collection = parsed.collection || 'page'; + config.collection = parsed.collection || 'page' // type is the value that the parser will look at to decide what to do with that file - config.type = parsed.type || 'html'; - config.options = parsed; - config.content = content.slice(end + 5); + config.type = parsed.type || 'html' + config.options = parsed + config.content = content.slice(end + 5) - if (parsed.layout) config.layout = parsed.layout; + if (parsed.layout) config.layout = parsed.layout } + } else { + config.content = content + config.type = 'html' + config.collection = 'page' } - return config; + return config } /** @@ -131,17 +156,16 @@ async function parseString(plugins, filePath, content) { * @param {String} content - file contents that could potentially contain options * @return {Object} - with the attributes options and content */ -async function parse(plugins, filePath) { - const content = await readFile(filePath, 'utf8'); +async function parse (plugins, filePath) { + const content = await readFile(filePath, 'utf8') const config = { filePath, options: {}, content: content - }; - + } - return Object.assign(config, await parseString(plugins, filePath, content)); + return Object.assign(config, await parseString(plugins, filePath, content)) } /** @@ -151,25 +175,25 @@ async function parse(plugins, filePath) { * @param {Number} ms - time in milleseconds * @return {String} - human readable string */ -const s = 1000; -const m = s * 60; -const h = m * 60; -const d = h * 24; +const s = 1000 +const m = s * 60 +const h = m * 60 +const d = h * 24 -function ms(ms) { +function ms (ms) { if (ms >= d) { - return `${Math.floor(ms / d)}d`; + return `${Math.floor(ms / d)}d` } if (ms >= h) { - return `${Math.floor(ms / h)}h`; + return `${Math.floor(ms / h)}h` } if (ms >= m) { - return `${Math.floor(ms / m)}m`; + return `${Math.floor(ms / m)}m` } if (ms >= s) { - return `${Math.floor(ms / s)}s`; + return `${Math.floor(ms / s)}s` } - return ms.toFixed(4).replace(/\.0000$/, '') + 'ms'; + return ms.toFixed(4).replace(/\.0000$/, '') + 'ms' } /** @@ -180,97 +204,111 @@ function ms(ms) { * @param {Object} additional - additional data that needs to be merged into template data * @return {String} - rendered template to string */ -async function render(plugins, templates, template, additional = {}) { - const start = process.hrtime(); +async function render (plugins, templates, template, additional = {}) { + const start = process.hrtime() - const { filePath } = template; - let { content = '' } = template; + const { filePath } = template + let { content = '' } = template // combine the data from the template and whatever is passed in - const data = merge(additional, template); - if(!content) return; + const data = merge(additional, template) + if (!content) return try { // let's run through rendering any plugin data that was parsed out - const pluginNames = Object.keys(plugins); - const tempDepends = []; - for(var p = 0; p < pluginNames.length; p++) { - const name = pluginNames[p]; - const plugin = plugins[name]; - - if(data[name] && data[name].length > 0) { - for(const found of data[name]) { - const output = await plugin.render(plugins, filePath, content, templates, data, found); - - if(output.content) content = output.content; - if(output.depends) tempDepends.push(output.depends); + const pluginNames = Object.keys(plugins) + const tempDepends = [] + for (var p = 0; p < pluginNames.length; p++) { + const name = pluginNames[p] + const plugin = plugins[name] + + if (data[name] && data[name].length > 0) { + for (const found of data[name]) { + if (!plugin.render) continue + + const output = await plugin.render(plugins, filePath, content, templates, data, found) + + if (output.content) content = output.content + if (output.depends) tempDepends.push(output.depends) } } } - if(tempDepends.length > 0) { - data.depends = [].concat.apply([], tempDepends); + if (tempDepends.length > 0) { + data.depends = [].concat.apply([], tempDepends) } - const templ = content.replace(/[\r\t\n]/g, ' '); - const re = /{{[\s]*(.+?)[\s]*}}/g; - const reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g; + const templ = content.replace(/[\r\t\n]/g, ' ') + const re = /{{[\s]*(.+?)[\s]*}}/g + + let code = 'var r=[];\n' + let cursor = 0 + let match = '' - let code = 'var r=[];\n'; - let cursor = 0; - let match; + function add (line, js) { // eslint-disable-line no-inner-declarations + const reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g - function add(line, js) { - js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : - (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); - return add; + js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') + : (code += line !== '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '') + return add } - while (match = re.exec(templ)) { - add(templ.slice(cursor, match.index))(match[1], true); - cursor = match.index + match[0].length; + + while ((match = re.exec(templ)) !== null) { + add(templ.slice(cursor, match.index))(match[1], true) + cursor = match.index + match[0].length } - add(templ.substr(cursor, templ.length - cursor)); - code += 'return r.join("");'; + add(templ.substr(cursor, templ.length - cursor)) + code += 'return r.join("");' + /* eslint-disable no-new-func */ const rendered = new Function(` with(this) { ${code.replace(/[\r\t\n]/g, '')} } - `).bind(data)(); + `).bind(data)() + /* eslint-enable no-new-func */ // if this template has a layout file we must render this then the layout file - const layout = template.layout && templates[template.layout]; + const layout = template.layout && templates[template.layout] // why are we doing this, well we pass around data a fairbit and we want to snapshot the state of the depends field at this point - const depends = data.depends ? JSON.parse(JSON.stringify(data.depends)) : []; + const depends = data.depends ? JSON.parse(JSON.stringify(data.depends)) : [] + + const safeData = {} + Object.keys(data).forEach((k) => { + if (k !== 'depends' && k !== 'layout') { + safeData[k] = data[k] + } + }) if (layout) { // we have to delete the old layout - delete data['layout']; + delete data['layout'] const output = await render(plugins, templates, layout, Object.assign({ - child: rendered, - depends: [] // pass in an empty data to stop from creating duplicate data - }, data)); + child: rendered + }, safeData)) return { + data: JSON.parse(JSON.stringify(safeData)), filePath, depends: output, rendered: output.rendered, - time: process.hrtime(start)[1]/1000000 - }; + time: process.hrtime(start)[1] / 1000000 + } } else { return { + data: JSON.parse(JSON.stringify(safeData)), filePath, rendered, depends, // this is if any plugin has added dependency information to the template - time: process.hrtime(start)[1]/1000000 - }; + time: process.hrtime(start)[1] / 1000000 + } } } catch (ex) { throw new Error(JSON.stringify({ error: `Error building template ${filePath}`, content: content, stack: ex.stack - })); + })) } } @@ -281,24 +319,24 @@ async function render(plugins, templates, template, additional = {}) { * @param {Object|Array} source - an array or object, must be the same type as the other value being passed * @return {Object|Array} - an array or object, depending on what has been passed in */ -function merge(target, source) { - if (typeof target == 'object' && typeof source == 'object') { +function merge (target, source) { + if (typeof target === 'object' && typeof source === 'object') { for (const key in source) { if (source[key] === null && (target[key] === undefined || target[key] === null)) { - target[key] = null; + target[key] = null } else if (source[key] instanceof Array) { - if (!target[key]) target[key] = []; - //concatenate arrays - target[key] = target[key].concat(source[key]); - } else if (typeof source[key] == 'object') { - if (!target[key]) target[key] = {}; - merge(target[key], source[key]); + if (!target[key]) target[key] = [] + // concatenate arrays + target[key] = target[key].concat(source[key]) + } else if (typeof source[key] === 'object') { + if (!target[key]) target[key] = {} + merge(target[key], source[key]) } else { - target[key] = source[key]; + target[key] = source[key] } } } - return target; + return target } /** @@ -306,21 +344,21 @@ function merge(target, source) { * @param {Object} item - rendered item output by render method * @return {Number} - time in milleseconds */ -function getTotalTimeOfDepends(item) { - let time = 0; +function getTotalTimeOfDepends (item) { + let time = 0 - if(Array.isArray(item)) { - time += item.map((i) => getTotalTimeOfDepends(i)).reduce((a, b) => a + b, 0); + if (Array.isArray(item)) { + time += item.map((i) => getTotalTimeOfDepends(i)).reduce((a, b) => a + b, 0) } - if(!Array.isArray(item) && typeof item === 'object') { - time += item.time; + if (!Array.isArray(item) && typeof item === 'object') { + time += item.time } // recursively find the depends values - if(item.depends) time += getTotalTimeOfDepends(item.depends); + if (item.depends) time += getTotalTimeOfDepends(item.depends) - return time; + return time } /** @@ -329,20 +367,20 @@ function getTotalTimeOfDepends(item) { * @param {Number} level - the level of the tree that the element is a part of (by default is 0) * @return {String} - ascii representation of render tree for the given item */ -function renderSubDepends(item, level=0) { - let output = ''; +function renderSubDepends (item, level = 0) { + let output = '' - if(Array.isArray(item)) { - output += item.map((i) => renderSubDepends(i, level)).join(''); + if (Array.isArray(item)) { + output += item.map((i) => renderSubDepends(i, level)).join('') } - if(!Array.isArray(item) && typeof item === 'object') { - output += '\n' + `${level === 0 ? '' : ' '.repeat(level * 2) + '-'} ${item.filePath} [${ms(item.time)}]`; + if (!Array.isArray(item) && typeof item === 'object') { + output += '\n' + `${level === 0 ? '' : ' '.repeat(level * 2) + '-'} ${item.filePath} [${ms(item.time)}]` } - if(item.depends) output += renderSubDepends(item.depends, level + 1); + if (item.depends) output += renderSubDepends(item.depends, level + 1) - return output ; + return output } module.exports = { @@ -352,8 +390,10 @@ module.exports = { render, ms, getConfig, + escapeRegexValues, ensureDirectoryExists, copyDirectory, renderSubDepends, + templateToString, getTotalTimeOfDepends -}; +} diff --git a/package.json b/package.json index 87602c8..1a42d3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sweeney", - "version": "1.1.0", + "version": "1.2.0", "description": "💈 A static site generator that cuts the way you want it to", "author": "Gabriel J. Csapo ", "license": "Apache-2.0", @@ -14,7 +14,7 @@ }, "homepage": "https://www.gabrielcsapo.com/sweeney", "scripts": { - "lint": "eslint .", + "lint": "standard --verbose | snazzy", "pretest": "rm -rf test/tmp", "test": "tape test/*.js | tap-diff", "coverage": "npm run pretest && tap test --coverage --coverage-report=lcov", @@ -29,19 +29,28 @@ ], "all": true }, + "standard": { + "ignore": [ + "docs/**", + "example/**", + "coverage/**" + ] + }, "bin": { "sweeney": "./bin/sweeney.js" }, "devDependencies": { - "eslint": "^4.12.1", "jsdoc": "^3.5.5", "markdown-it": "^8.4.0", "minami": "^1.2.3", - "tap": "^11.0.0", + "snazzy": "^7.1.1", + "standard": "^11.0.1", + "tap": "^11.1.3", "tap-diff": "^0.1.1", "tape": "^4.8.0" }, "dependencies": { + "formidable": "^1.2.1", "woof": "^0.3.0" } } diff --git a/test/defaultPlugins.js b/test/defaultPlugins.js index 08fa7db..82fb2b6 100644 --- a/test/defaultPlugins.js +++ b/test/defaultPlugins.js @@ -1,16 +1,41 @@ -const test = require('tape'); -const path = require('path'); +const test = require('tape') +const path = require('path') -const defaultPlugins = require('../lib/defaultPlugins'); +const defaultPlugins = require('../lib/defaultPlugins') test('@defaultPlugins', (t) => { - t.plan(1); + t.plan(2) + + t.test('@editable', (t) => { + t.plan(1) + + t.test('should be able to parse valid markup', async (t) => { + const parse = defaultPlugins['editable'].parse + + const output = await parse(path.resolve(__dirname, 'fixtures', 'includes.sy'), ` +
+ {{-- editable blue string --}} +
+ `) + + t.deepEqual(output, { + content: '\n
\n {{ blue }}\n
\n ', + found: [{ + filePath: path.resolve(__dirname, 'fixtures', 'includes.sy'), + variableName: 'blue', + type: 'string' + }] + }) + + t.end() + }) + }) t.test('@includes', (t) => { - t.plan(2); + t.plan(2) t.test('should parse file with css and sy paths correctly', async (t) => { - const parse = defaultPlugins['includes'].parse; + const parse = defaultPlugins['includes'].parse const output = await parse(path.resolve(__dirname, 'fixtures', 'includes.sy'), ` @@ -41,36 +66,36 @@ test('@defaultPlugins', (t) => {
- `); + `) t.deepEqual(output, { content: `\n \n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${path.resolve(__dirname)}/site.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname)}/fixtures/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n `, found: [ path.resolve(__dirname, 'site.css'), - path.resolve(__dirname, 'fixtures', 'nav.sy'), + path.resolve(__dirname, 'fixtures', 'nav.sy') ] - }); + }) - t.end(); - }); + t.end() + }) t.test('should be able to render parsed template', async (t) => { - const render = defaultPlugins['includes'].render; + const render = defaultPlugins['includes'].render const output = await render({ - includes: defaultPlugins['includes'] - }, - path.resolve(__dirname, 'fixtures', 'includes.sy'), - `\n \n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${path.resolve(__dirname)}/fixtures/test.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname)}/fixtures/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n `, [], {}, - path.resolve(__dirname, 'fixtures', 'test.css') - ); - - t.deepEqual(output.depends.length, 1); - t.equal(output.depends[0].filePath, path.resolve(__dirname, 'fixtures', 'test.css')); - - t.deepEqual(output.content, `\n \n\n \n \n \n \n\n {{ options.title || site.title }}\n \n \n \n\n\n \n {{-- includes ${path.resolve(__dirname)}/fixtures/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n `); - - t.end(); - }); - }); -}); + includes: defaultPlugins['includes'] + }, + path.resolve(__dirname, 'fixtures', 'includes.sy'), + `\n \n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${path.resolve(__dirname)}/fixtures/test.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname)}/fixtures/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n `, [], {}, + path.resolve(__dirname, 'fixtures', 'test.css') + ) + + t.deepEqual(output.depends.length, 1) + t.equal(output.depends[0].filePath, path.resolve(__dirname, 'fixtures', 'test.css')) + + t.deepEqual(output.content, `\n \n\n \n \n \n \n\n {{ options.title || site.title }}\n \n \n \n\n\n \n {{-- includes ${path.resolve(__dirname)}/fixtures/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n `) + + t.end() + }) + }) +}) diff --git a/test/site.js b/test/site.js index 1ea1467..ced9ef8 100644 --- a/test/site.js +++ b/test/site.js @@ -1,166 +1,174 @@ -const test = require('tape'); -const path = require('path'); +/* eslint-disable no-template-curly-in-string */ -const Site = require('../lib/site'); +const test = require('tape') +const path = require('path') + +const Site = require('../lib/site') test('@site', (t) => { - t.plan(3); + t.plan(3) t.test('should be able to get a instance of site without any data being passed to it', (t) => { - const site = new Site(); - t.equal(site.source, path.resolve(__dirname, '..')); - t.equal(site.output, path.resolve(__dirname, '..', 'site')); - t.deepEqual(site.files, []); - t.deepEqual(site.rendered, []); - t.deepEqual(site.config, {}); - t.ok(site.plugins['includes']); - t.equal(typeof site.plugins['includes']['parse'], 'function'); - t.equal(typeof site.plugins['includes']['render'], 'function'); + const site = new Site() + t.equal(site.source, path.resolve(__dirname, '..')) + t.equal(site.output, path.resolve(__dirname, '..', 'site')) + t.deepEqual(site.files, []) + t.deepEqual(site.rendered, []) + t.deepEqual(site.config, {}) + t.ok(site.plugins['includes']) + t.equal(typeof site.plugins['includes']['parse'], 'function') + t.equal(typeof site.plugins['includes']['render'], 'function') - t.end(); - }); + t.end() + }) t.test('@crawl', (t) => { - t.plan(1); + t.plan(1) t.test('should be able to crawl fixtures directory properly', async (t) => { const site = new Site({ - source: path.resolve(__dirname, 'fixtures') - }); + source: path.resolve(__dirname, 'fixtures'), + output: path.resolve(__dirname, 'output') + }) - await site.crawl(); + await site.crawl() t.deepEqual(site.files, [{ - 'filePath': `${path.resolve(__dirname, 'fixtures')}/depend.sy`, - 'options': { - 'type': 'page' - }, - 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, - 'name': 'depend', - 'includes': [ - `${path.resolve(__dirname, 'fixtures')}/render.sy` - ], - 'collection': 'page', + 'filePath': `${path.resolve(__dirname, 'fixtures')}/depend.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/depend.html`, + 'options': { 'type': 'page' }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/includes.sy`, - 'options': { - 'type': 'layout' - }, - 'content': `\n\n\n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${__dirname}/site.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname, 'fixtures')}/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n\n`, - 'name': 'includes', - 'includes': [ - `${path.resolve(__dirname)}/site.css`, - `${path.resolve(__dirname, 'fixtures')}/nav.sy` - ], - 'collection': 'page', + 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, + 'name': 'depend', + 'includes': [ + `${path.resolve(__dirname, 'fixtures')}/render.sy` + ], + 'collection': 'page', + 'type': 'page' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/includes.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/includes.html`, + 'options': { 'type': 'layout' }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/render.sy`, - 'options': { - 'title': 'Welcome to Sweeney!', - 'tags': [ - 'sweeney', - 'example' - ] - }, - 'content': '
\n
    \n {{ options.tags.map((tag) => `
  • ${tag}
  • `).join(\'\') }}\n
\n
\n', - 'name': 'render', - 'collection': 'page', - 'type': 'html' + 'content': `\n\n\n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${__dirname}/site.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname, 'fixtures')}/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n\n`, + 'name': 'includes', + 'includes': [ + `${path.resolve(__dirname)}/site.css`, + `${path.resolve(__dirname, 'fixtures')}/nav.sy` + ], + 'collection': 'page', + 'type': 'layout' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/render.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/render.html`, + 'options': { + 'title': 'Welcome to Sweeney!', + 'tags': [ + 'sweeney', + 'example' + ] }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/sub.sy`, - 'options': { - 'type': 'page' - }, - 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/depend.sy --}}\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, - 'name': 'sub', - 'includes': [ - `${path.resolve(__dirname, 'fixtures')}/depend.sy`, - `${path.resolve(__dirname, 'fixtures')}/render.sy` - ], - 'collection': 'page', + 'content': '
\n
    \n {{ options.tags.map((tag) => `
  • ${tag}
  • `).join(\'\') }}\n
\n
\n', + 'name': 'render', + 'collection': 'page', + 'type': 'html' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/sub.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/sub.html`, + 'options': { 'type': 'page' }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/test.sy`, - 'options': { - 'title': 'Welcome to Sweeney!', - 'tags': [ - 'sweeney', - 'example' - ] - }, - 'content': `\n\n\n{{-- includes ${__dirname}/partials/head.html --}}\n\n\n\n{{-- includes ${__dirname}/partials/header.html --}}\n\n
\n
\n {{ content }}\n
\n
\n\n{{-- includes ${__dirname}/partials/footer.html --}}\n\n\n\n\n`, - 'name': 'test', - 'includes': [ - `${__dirname}/partials/head.html`, - `${__dirname}/partials/header.html`, - `${__dirname}/partials/footer.html` - ], - 'collection': 'page', - 'type': 'html' + 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/depend.sy --}}\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, + 'name': 'sub', + 'includes': [ + `${path.resolve(__dirname, 'fixtures')}/depend.sy`, + `${path.resolve(__dirname, 'fixtures')}/render.sy` + ], + 'collection': 'page', + 'type': 'page' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/test.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/test.html`, + 'options': { + 'title': 'Welcome to Sweeney!', + 'tags': [ + 'sweeney', + 'example' + ] }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/throws-error.sy`, - 'options': { - 'title': 'Welcome to Sweeney!', - 'tags': [ - 'sweeney', - 'example' - ] - }, - 'content': '
\n
    \n {{ tagss.map((tag) => `
  • ${tag}
  • `)}}\n
\n
\n', - 'name': 'throws-error', - 'collection': 'page', - 'type': 'html' - } - ]); - - t.end(); - }); + 'content': `\n\n\n{{-- includes ${__dirname}/partials/head.html --}}\n\n\n\n{{-- includes ${__dirname}/partials/header.html --}}\n\n
\n
\n {{ content }}\n
\n
\n\n{{-- includes ${__dirname}/partials/footer.html --}}\n\n\n\n\n`, + 'name': 'test', + 'includes': [ + `${__dirname}/partials/head.html`, + `${__dirname}/partials/header.html`, + `${__dirname}/partials/footer.html` + ], + 'collection': 'page', + 'type': 'html' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/throws-error.sy`, + 'outputPath': `${path.resolve(__dirname, 'output')}/throws-error.html`, + 'options': { + 'title': 'Welcome to Sweeney!', + 'tags': [ + 'sweeney', + 'example' + ] + }, + 'content': '
\n
    \n {{ tagss.map((tag) => `
  • ${tag}
  • `)}}\n
\n
\n', + 'name': 'throws-error', + 'collection': 'page', + 'type': 'html' + } + ]) - }); + t.end() + }) + }) t.test('@categorize', (t) => { - t.plan(1); + t.plan(1) t.test('should be able to categorize a mix of pages and layouts', (t) => { const site = new Site({ source: path.resolve(__dirname, 'fixtures') - }); + }) const files = [{ - 'filePath': `${path.resolve(__dirname, 'fixtures')}/depend.sy`, - 'options': { - 'type': 'page' - }, - 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, - 'name': 'depend', - 'includes': [ - `${path.resolve(__dirname, 'fixtures')}/render.sy` - ], - 'collection': 'page', + 'filePath': `${path.resolve(__dirname, 'fixtures')}/depend.sy`, + 'options': { 'type': 'page' }, - { - 'filePath': `${path.resolve(__dirname, 'fixtures')}/includes.sy`, - 'options': { - 'type': 'layout' - }, - 'content': `\n\n\n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${__dirname}/site.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname, 'fixtures')}/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n\n`, - 'name': 'includes', - 'includes': [ - `${path.resolve(__dirname)}/site.css`, - `${path.resolve(__dirname, 'fixtures')}/nav.sy` - ], - 'collection': 'page', + 'content': `\n
\n {{-- includes ${path.resolve(__dirname, 'fixtures')}/render.sy --}}\n
\n`, + 'name': 'depend', + 'includes': [ + `${path.resolve(__dirname, 'fixtures')}/render.sy` + ], + 'collection': 'page', + 'type': 'page' + }, + { + 'filePath': `${path.resolve(__dirname, 'fixtures')}/includes.sy`, + 'options': { 'type': 'layout' - } - ]; + }, + 'content': `\n\n\n\n \n \n \n \n\n {{ options.title || site.title }}\n {{-- includes ${__dirname}/site.css --}}\n \n \n\n\n \n {{-- includes ${path.resolve(__dirname, 'fixtures')}/nav.sy --}}\n\n
\n
\n
\n {{ child }}\n
\n\n \n
\n \n\n\n`, + 'name': 'includes', + 'includes': [ + `${path.resolve(__dirname)}/site.css`, + `${path.resolve(__dirname, 'fixtures')}/nav.sy` + ], + 'collection': 'page', + 'type': 'layout' + } + ] t.deepEqual(site.categorize(files), { layouts: { @@ -194,9 +202,9 @@ test('@site', (t) => { 'type': 'page' } } - }); + }) - t.end(); - }); - }); -}); + t.end() + }) + }) +}) diff --git a/test/util.js b/test/util.js index febe7cd..1c5e326 100644 --- a/test/util.js +++ b/test/util.js @@ -1,30 +1,20 @@ -const fs = require('fs'); -const path = require('path'); -const test = require('tape'); - -const { promisify } = require('util'); -const stat = promisify(fs.stat); -const readdir = promisify(fs.readdir); - -const { - ms, - merge, - render, - parse, - parseString, - getConfig, - ensureDirectoryExists, - renderSubDepends, - getTotalTimeOfDepends, - copyDirectory -} = require('../lib/util'); - -const defaultPlugins = require('../lib/defaultPlugins'); +/* eslint-disable no-template-curly-in-string */ -test('util', (t) => { +const fs = require('fs') +const path = require('path') +const test = require('tape') + +const { promisify } = require('util') +const stat = promisify(fs.stat) +const readdir = promisify(fs.readdir) + +const { ms, merge, render, escapeRegexValues, parse, parseString, getConfig, ensureDirectoryExists, renderSubDepends, getTotalTimeOfDepends, templateToString, copyDirectory } = require('../lib/util') +const defaultPlugins = require('../lib/defaultPlugins') + +test('util', (t) => { t.test('@merge', (t) => { - t.plan(2); + t.plan(2) t.test('should be able to merge nested objects', (t) => { const merged = merge({ @@ -35,16 +25,16 @@ test('util', (t) => { d: { f: 'b' } - }); + }) t.deepEqual(merged, { d: { t: 'hi', f: 'b' } - }); - t.end(); - }); + }) + t.end() + }) t.test('should be able to merge arrays of objects', (t) => { const merged = merge({ @@ -57,7 +47,7 @@ test('util', (t) => { f: 'b', c: ['foo', 'bar'] } - }); + }) t.deepEqual(merged, { d: { @@ -65,70 +55,86 @@ test('util', (t) => { f: 'b', c: ['bob', 'foo', 'bar'] } - }); - t.end(); - }); - }); + }) + t.end() + }) + }) + + t.test('@escapeRegexValues', (t) => { + t.plan(2) + + t.test('should escape ? and ! properly', (t) => { + const output = escapeRegexValues('What?!') + t.equal(output, 'What\\?\\!') + t.end() + }) + + t.test('should escape + and . properly', (t) => { + const output = escapeRegexValues('What?!.+') + t.equal(output, 'What\\?\\!\\.\\+') + t.end() + }) + }) t.test('@render', (t) => { - t.plan(3); + t.plan(3) t.test('should throw an error with template', async (t) => { - const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/throws-error.sy')); + const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/throws-error.sy')) try { - const rendered = await render(defaultPlugins, [], parsed, {}); - t.fail(`${rendered} should not be rendered`); + const rendered = await render(defaultPlugins, [], parsed, {}) + t.fail(`${rendered} should not be rendered`) } catch (ex) { - const error = JSON.parse(ex.message); + const error = JSON.parse(ex.message) - t.deepEqual(Object.keys(error), ['error', 'content', 'stack']); - t.equal(error.content, '
\n
    \n {{ tagss.map((tag) => `
  • ${tag}
  • `)}}\n
\n
\n'); - t.equal(error.stack.substr(0, 36), 'ReferenceError: tagss is not defined'); + t.deepEqual(Object.keys(error), ['error', 'content', 'stack']) + t.equal(error.content, '
\n
    \n {{ tagss.map((tag) => `
  • ${tag}
  • `)}}\n
\n
\n') + t.equal(error.stack.substr(0, 36), 'ReferenceError: tagss is not defined') - t.end(); + t.end() } - }); + }) t.test('should render template properly', async (t) => { - const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/render.sy')); - const rendered = await render(defaultPlugins, [], parsed, {}); + const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/render.sy')) + const rendered = await render(defaultPlugins, [], parsed, {}) - t.deepEqual(Object.keys(rendered), ['filePath', 'rendered', 'depends', 'time']); - t.equal(typeof rendered.time, 'number'); - t.deepEqual(rendered.depends, []); - t.equal(rendered.rendered, '
  • sweeney
  • example
'); - t.equal(rendered.filePath, path.resolve(__dirname, './fixtures/render.sy')); + t.deepEqual(Object.keys(rendered), ['data', 'filePath', 'rendered', 'depends', 'time']) + t.equal(typeof rendered.time, 'number') + t.deepEqual(rendered.depends, []) + t.equal(rendered.rendered, '
  • sweeney
  • example
') + t.equal(rendered.filePath, path.resolve(__dirname, './fixtures/render.sy')) - t.end(); - }); + t.end() + }) t.test('should be able to properly add depends to template when rendering sub templates', async (t) => { - const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/sub.sy')); - const rendered = await render(defaultPlugins, [], parsed, {}); - - t.equal(rendered.filePath, path.resolve(__dirname, './fixtures/sub.sy')); - t.equal(rendered.rendered, '
  • sweeney
  • example
  • sweeney
  • example
  • sweeney
  • example
'); - t.equal(rendered.depends.length, 2); - t.equal(rendered.depends[0].filePath, path.resolve(__dirname, './fixtures/depend.sy')); - t.equal(rendered.depends[0].rendered, '
  • sweeney
  • example
'); - t.equal(rendered.depends[0].depends.length, 1); - t.equal(rendered.depends[0].depends[0].filePath, path.resolve(__dirname, './fixtures/render.sy')); - t.equal(rendered.depends[0].depends[0].rendered, '
  • sweeney
  • example
'); - t.equal(rendered.depends[0].depends[0].depends.length, 0); - - t.equal(rendered.depends[1].filePath, path.resolve(__dirname, './fixtures/render.sy')); - t.equal(rendered.depends[1].rendered, '
  • sweeney
  • example
  • sweeney
  • example
'); - t.equal(rendered.depends[1].depends.length, 0); - t.end(); - }); - }); + const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/sub.sy')) + const rendered = await render(defaultPlugins, [], parsed, {}) + + t.equal(rendered.filePath, path.resolve(__dirname, './fixtures/sub.sy')) + t.equal(rendered.rendered, '
  • sweeney
  • example
  • sweeney
  • example
  • sweeney
  • example
') + t.equal(rendered.depends.length, 2) + t.equal(rendered.depends[0].filePath, path.resolve(__dirname, './fixtures/depend.sy')) + t.equal(rendered.depends[0].rendered, '
  • sweeney
  • example
') + t.equal(rendered.depends[0].depends.length, 1) + t.equal(rendered.depends[0].depends[0].filePath, path.resolve(__dirname, './fixtures/render.sy')) + t.equal(rendered.depends[0].depends[0].rendered, '
  • sweeney
  • example
') + t.equal(rendered.depends[0].depends[0].depends.length, 0) + + t.equal(rendered.depends[1].filePath, path.resolve(__dirname, './fixtures/render.sy')) + t.equal(rendered.depends[1].rendered, '
  • sweeney
  • example
  • sweeney
  • example
') + t.equal(rendered.depends[1].depends.length, 0) + t.end() + }) + }) t.test('@parse', (t) => { - t.plan(1); + t.plan(1) - t.test('@parse: should be able to parse file with options', (async (t) => { - const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/test.sy')); + t.test('should be able to parse file with options', async (t) => { + const parsed = await parse(defaultPlugins, path.resolve(__dirname, './fixtures/test.sy')) t.deepEqual({ filePath: path.resolve(__dirname, 'fixtures/test.sy'), @@ -145,32 +151,54 @@ test('util', (t) => { ], collection: 'page', type: 'html' - }, parsed); + }, parsed) + + t.end() + }) + }) + + t.test('@templateToString', (t) => { + t.plan(1) + + t.test('should be able to turn template object back into a string', (t) => { + const template = { + name: 'default', + includes: ['/foo/partials/head.html', '/foo/partials/header.html', '/foo/partials/footer.html'], + collection: 'page', + type: 'html', + options: { + title: 'Welcome to Sweeney!', + tags: ['sweeney', 'example'] + }, + content: ' \n \n\n {{-- includes /foo/partials/head.html --}}\n\n \n\n {{-- includes /foo/partials/header.html --}}\n\n
\n
\n {{ content }}\n
\n
\n\n {{-- includes /foo/partials/footer.html --}}\n\n \n\n \n' + } - t.end(); - })); - }); + t.equal(templateToString(template), '---\n{\n "title": "Welcome to Sweeney!",\n "tags": [\n "sweeney",\n "example"\n ],\n "type": "html"\n}\n---\n \n \n\n {{-- includes /foo/partials/head.html --}}\n\n \n\n {{-- includes /foo/partials/header.html --}}\n\n
\n
\n {{ content }}\n
\n
\n\n {{-- includes /foo/partials/footer.html --}}\n\n \n\n ') + + t.end() + }) + }) t.test('@parseString', (t) => { - t.plan(3); + t.plan(4) t.test('should be able to string with options', async (t) => { - const parsed = await parseString(defaultPlugins, '/foo/bar/something.sy', '---\n{ "layout": "post", "title": "Welcome to Jekyll!", "date": "2014-10-18 12:58:29", "categories": "jekyll update" }\n---\n# Hello world'); + const parsed = await parseString(defaultPlugins, '/foo/bar/something.sy', '---\n{ "layout": "post", "title": "Welcome to Sweeney!", "date": "2014-10-18 12:58:29", "categories": "sweeney update" }\n---\n# Hello world') - t.equal(parsed.name, 'something'); - t.equal(parsed.layout, 'post'); + t.equal(parsed.name, 'something') + t.equal(parsed.layout, 'post') t.deepEqual(parsed.options, { layout: 'post', - title: 'Welcome to Jekyll!', + title: 'Welcome to Sweeney!', date: '2014-10-18 12:58:29', - categories: 'jekyll update' - }); - t.equal(parsed.content, '# Hello world'); + categories: 'sweeney update' + }) + t.equal(parsed.content, '# Hello world') - t.end(); - }); + t.end() + }) - t.test('should be able to parse sweeney tags', async (t) => { + t.test('should be able to parse tags', async (t) => { const parsed = await parseString(defaultPlugins, '/foo/bar/default.sy', `--- { "title": "Welcome to Sweeney!", @@ -200,7 +228,7 @@ test('util', (t) => { -`); +`) t.deepEqual({ name: 'default', @@ -212,116 +240,138 @@ test('util', (t) => { tags: ['sweeney', 'example'] }, content: ' \n \n\n {{-- includes /foo/partials/head.html --}}\n\n \n\n {{-- includes /foo/partials/header.html --}}\n\n
\n
\n {{ content }}\n
\n
\n\n {{-- includes /foo/partials/footer.html --}}\n\n \n\n \n' - }, parsed); + }, parsed) - t.end(); - }); + t.end() + }) + + t.test('should work with an empty template', async (t) => { + const parsed = await parseString({ + test: {} + }, '/foo/bar/default.sy', ` +
+ should do something +
+ `) + + t.deepEqual(parsed, { + name: 'default', + content: '\n
\n should do something\n
\n ', + type: 'html', + collection: 'page' + }) + + t.end() + }) t.test('should work with custom plugins object', async (t) => { const parsed = await parseString({ test: { - parse(filePath, content) { - const reg = /{{-- test\('(.+?)'\) --}}/g; + parse (filePath, content) { + const reg = /{{-- test\('(.+?)'\) --}}/g if (content.match(reg)) { - const found = []; - let block; + const found = [] + let block while ((block = reg.exec(content)) != null) { - found.push(block[1]); + found.push(block[1]) } return { content, found - }; + } } - return false; + return false } } }, '/foo/bar/default.sy', `
{{-- test('should do something') --}}
- `); + `) t.deepEqual(parsed, { name: 'default', - test: ['should do something'] - }); + test: ['should do something'], + content: '\n
\n {{-- test(\'should do something\') --}}\n
\n ', + type: 'html', + collection: 'page' + }) - t.end(); - }); - }); + t.end() + }) + }) - t.test('@ensureDirectoryExists', (async (t) => { - const dir = path.resolve(__dirname, 'tmp', 'really', 'not', 'here'); + t.test('@ensureDirectoryExists', async (t) => { + const dir = path.resolve(__dirname, 'tmp', 'really', 'not', 'here') - await ensureDirectoryExists(dir); + await ensureDirectoryExists(dir) try { - await stat(dir); - t.end(); + await stat(dir) + t.end() } catch (ex) { - t.fail(ex); + t.fail(ex) } - })); + }) t.test('@getConfig', (t) => { - t.plan(4); + t.plan(4) t.test('should be able to read .sweeney file that exists', async (t) => { - const dir = path.resolve(__dirname, 'fixtures', 'config', 'export'); - const config = await getConfig(dir); + const dir = path.resolve(__dirname, 'fixtures', 'config', 'export') + const config = await getConfig(dir) - t.equal(typeof config, 'object'); - t.end(); - }); + t.equal(typeof config, 'object') + t.end() + }) t.test('should be able to read .sweeney file that returns a promise', async (t) => { - const dir = path.resolve(__dirname, 'fixtures', 'config', 'promise'); - const config = await getConfig(dir); + const dir = path.resolve(__dirname, 'fixtures', 'config', 'promise') + const config = await getConfig(dir) - t.equal(typeof config, 'object'); + t.equal(typeof config, 'object') t.deepEqual(config, { foo: { hello: 'world' } - }); - t.end(); - }); + }) + t.end() + }) t.test('should not throw an error if no config is found', async (t) => { try { - const config = await getConfig(__dirname); - t.deepEqual(config, {}); - t.end(); + const config = await getConfig(__dirname) + t.deepEqual(config, {}) + t.end() } catch (ex) { - t.fail(ex); + t.fail(ex) } - }); + }) t.test('should propogate error to user if the config has an error', async (t) => { - const dir = path.resolve(__dirname, 'fixtures', 'config', 'throw'); + const dir = path.resolve(__dirname, 'fixtures', 'config', 'throw') try { - const config = await getConfig(dir); - t.fail(config); + const config = await getConfig(dir) + t.fail(config) } catch (ex) { - t.equal(ex.message, 'I am broken!'); - t.ok(ex.stack.indexOf(dir) > -1, 'ensure the path to the config is in the error (retain the error object, don\'t alter it)'); - t.end(); + t.equal(ex.message, 'I am broken!') + t.ok(ex.stack.indexOf(dir) > -1, 'ensure the path to the config is in the error (retain the error object, don\'t alter it)') + t.end() } - }); - }); + }) + }) t.test('@copyDirectory', (t) => { - t.plan(1); + t.plan(1) t.test('should be able to copy example directory and all its content', async (t) => { - const destination = path.resolve(__dirname, 'tmp', 'copy'); - const source = path.resolve(__dirname, '..', 'example'); - await copyDirectory(source, destination); + const destination = path.resolve(__dirname, 'tmp', 'copy') + const source = path.resolve(__dirname, '..', 'example') + await copyDirectory(source, destination) - const files = await readdir(destination); + const files = await readdir(destination) t.deepEqual(files, [ '.sweeney', @@ -333,24 +383,24 @@ test('util', (t) => { 'projects.sy', 'site.css', 'sweeney.svg' - ]); + ]) // ensure sub directories have files - const subFiles = await readdir(path.resolve(destination, 'layouts')); + const subFiles = await readdir(path.resolve(destination, 'layouts')) t.deepEqual(subFiles, [ 'default.sy', 'nav.sy', 'page.sy', 'post.sy' - ]); + ]) - t.end(); - }); - }); + t.end() + }) + }) t.test('@getTotalTimeOfDepends', (t) => { - t.plan(1); + t.plan(1) const rendered = { filePath: 'foo', @@ -374,18 +424,17 @@ test('util', (t) => { time: 50 }] }] - }; + } t.test('should be able to get all of the times aggregated', (t) => { - const time = getTotalTimeOfDepends(rendered); - t.equal(time, 1010); - t.end(); - }); - - }); + const time = getTotalTimeOfDepends(rendered) + t.equal(time, 1010) + t.end() + }) + }) t.test('@renderSubDepends', (t) => { - t.plan(4); + t.plan(4) const rendered = { filePath: 'foo', @@ -409,118 +458,117 @@ test('util', (t) => { time: 50 }] }] - }; + } t.test('should be able to resolve recursive dependencies', (t) => { - const output = renderSubDepends(rendered, 0); + const output = renderSubDepends(rendered, 0) - t.equal(output, '\n foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]'); + t.equal(output, '\n foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]') - t.end(); - }); + t.end() + }) t.test('should work when no starting level is given (default 0)', (t) => { - const output = renderSubDepends(rendered); + const output = renderSubDepends(rendered) - t.equal(output, '\n foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]'); + t.equal(output, '\n foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]') - t.end(); - }); + t.end() + }) t.test('should honor offset starting level at 1', (t) => { - const output = renderSubDepends(rendered, 1); + const output = renderSubDepends(rendered, 1) - t.equal(output, '\n - foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]'); + t.equal(output, '\n - foo [100ms]\n - boo [150ms]\n - let [250ms]\n - hoooot [330ms]\n - hoo [130ms]\n - hoot [50ms]') - t.end(); - }); + t.end() + }) t.test('should work with array of files', (t) => { const rendered = [{ - filePath: '~/sweeney/example/about.sy', - depends: { - filePath: '~/sweeney/example/layouts/default.sy', - time: 0.63868 - }, - time: 0.932518 + filePath: '~/sweeney/example/about.sy', + depends: { + filePath: '~/sweeney/example/layouts/default.sy', + time: 0.63868 }, - { - filePath: '~/sweeney/example/index.sy', - depends: { - filePath: '~/sweeney/example/layouts/default.sy', - time: 0.505146 - }, - time: 0.820841 + time: 0.932518 + }, + { + filePath: '~/sweeney/example/index.sy', + depends: { + filePath: '~/sweeney/example/layouts/default.sy', + time: 0.505146 }, - { - filePath: '~/sweeney/example/posts/2017-11-20-welcome-to-sweeney.sy', - depends: { - filePath: '~/sweeney/example/layouts/post.sy', - depends: [{ - filePath: '~/sweeney/example/index.sy', - depends: { - filePath: '~/sweeney/example/layouts/default.sy', - time: 0.505146 - }, - time: 0.820841 - }], - time: 1.35372 - }, - time: 1.84443 + time: 0.820841 + }, + { + filePath: '~/sweeney/example/posts/2017-11-20-welcome-to-sweeney.sy', + depends: { + filePath: '~/sweeney/example/layouts/post.sy', + depends: [{ + filePath: '~/sweeney/example/index.sy', + depends: { + filePath: '~/sweeney/example/layouts/default.sy', + time: 0.505146 + }, + time: 0.820841 + }], + time: 1.35372 }, - { - filePath: '~/sweeney/example/posts.sy', - depends: { - filePath: '~/sweeney/example/layouts/default.sy', - time: 1.0139 - }, - time: 2.090657 + time: 1.84443 + }, + { + filePath: '~/sweeney/example/posts.sy', + depends: { + filePath: '~/sweeney/example/layouts/default.sy', + time: 1.0139 }, - { - filePath: '~/sweeney/example/projects.sy', - depends: { - filePath: '~/sweeney/example/layouts/default.sy', - time: 0.482915 - }, - time: 0.934869 - } - ]; + time: 2.090657 + }, + { + filePath: '~/sweeney/example/projects.sy', + depends: { + filePath: '~/sweeney/example/layouts/default.sy', + time: 0.482915 + }, + time: 0.934869 + } + ] - const output = renderSubDepends(rendered, 1); + const output = renderSubDepends(rendered, 1) - t.equal(output, '\n - ~/sweeney/example/about.sy [0.9325ms]\n - ~/sweeney/example/layouts/default.sy [0.6387ms]\n - ~/sweeney/example/index.sy [0.8208ms]\n - ~/sweeney/example/layouts/default.sy [0.5051ms]\n - ~/sweeney/example/posts/2017-11-20-welcome-to-sweeney.sy [1.8444ms]\n - ~/sweeney/example/layouts/post.sy [1.3537ms]\n - ~/sweeney/example/index.sy [0.8208ms]\n - ~/sweeney/example/layouts/default.sy [0.5051ms]\n - ~/sweeney/example/posts.sy [2.0907ms]\n - ~/sweeney/example/layouts/default.sy [1.0139ms]\n - ~/sweeney/example/projects.sy [0.9349ms]\n - ~/sweeney/example/layouts/default.sy [0.4829ms]'); + t.equal(output, '\n - ~/sweeney/example/about.sy [0.9325ms]\n - ~/sweeney/example/layouts/default.sy [0.6387ms]\n - ~/sweeney/example/index.sy [0.8208ms]\n - ~/sweeney/example/layouts/default.sy [0.5051ms]\n - ~/sweeney/example/posts/2017-11-20-welcome-to-sweeney.sy [1.8444ms]\n - ~/sweeney/example/layouts/post.sy [1.3537ms]\n - ~/sweeney/example/index.sy [0.8208ms]\n - ~/sweeney/example/layouts/default.sy [0.5051ms]\n - ~/sweeney/example/posts.sy [2.0907ms]\n - ~/sweeney/example/layouts/default.sy [1.0139ms]\n - ~/sweeney/example/projects.sy [0.9349ms]\n - ~/sweeney/example/layouts/default.sy [0.4829ms]') - t.end(); - }); - }); + t.end() + }) + }) t.test('@ms', (t) => { - t.plan(5); + t.plan(5) t.test('milleseconds', (t) => { - t.equal(ms(600), '600ms'); - t.end(); - }); + t.equal(ms(600), '600ms') + t.end() + }) t.test('seconds', (t) => { - t.equal(ms(6000), '6s'); - t.end(); - }); + t.equal(ms(6000), '6s') + t.end() + }) t.test('minutes', (t) => { - t.equal(ms(600000), '10m'); - t.end(); - }); + t.equal(ms(600000), '10m') + t.end() + }) t.test('hours', (t) => { - t.equal(ms(6000000), '1h'); - t.end(); - }); + t.equal(ms(6000000), '1h') + t.end() + }) t.test('days', (t) => { - t.equal(ms(600000000), '6d'); - t.end(); - }); - }); - -}); + t.equal(ms(600000000), '6d') + t.end() + }) + }) +})