Full Code of JSRocksHQ/harmonic for AI

master c457e015632a cached
37 files
69.2 KB
18.5k tokens
49 symbols
1 requests
Download .txt
Repository: JSRocksHQ/harmonic
Branch: master
Commit: c457e015632a
Files: 37
Total size: 69.2 KB

Directory structure:
gitextract_a6js28w6/

├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.js
├── doc/
│   ├── README.md
│   ├── blogging.md
│   ├── config.md
│   ├── installing.md
│   ├── markdown-header.md
│   └── themes.md
├── entry_points/
│   ├── README.md
│   └── harmonic.js
├── gulpfile.js
├── package.json
└── src/
    ├── bin/
    │   ├── cli/
    │   │   ├── harmonic.js
    │   │   ├── logo.js
    │   │   └── util.js
    │   ├── client/
    │   │   ├── .babelrc
    │   │   └── harmonic-client.js
    │   ├── config.js
    │   ├── core.js
    │   ├── helpers.js
    │   ├── parser.js
    │   ├── resources/
    │   │   └── rss.xml
    │   ├── skeleton/
    │   │   ├── package.json
    │   │   ├── resources/
    │   │   │   └── readme.txt
    │   │   └── src/
    │   │       ├── pages/
    │   │       │   ├── en/
    │   │       │   │   └── about.md
    │   │       │   └── pt-br/
    │   │       │       └── about.md
    │   │       └── posts/
    │   │           ├── en/
    │   │           │   └── hello-world.md
    │   │           └── pt-br/
    │   │               └── hello-world.md
    │   └── theme.js
    └── test/
        └── main.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# editorconfig.org - unify code style
# plugins for text editors: editorconfig.org/#download
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true


================================================
FILE: .eslintrc
================================================
{
    "extends": "eslint:recommended",
    "parser": "babel-eslint",
    "env": {
        "node": true
    },
    "rules": {
       "strict": [2, "never"], // ES Modules are implicitly strict
       "no-use-before-define": [2, "nofunc"], // allow referencing function declarations in the whole scope where they're defined
       "no-var": 2,
       "curly": 2,
       "default-case": 2,
       "no-else-return": 2,
       "no-param-reassign": 2,
       "indent": [2, 4],
       "brace-style": 2,
       "comma-style": 2,
       "no-multiple-empty-lines": [2, { "max": 2 }],
       "quotes": [2, "single"],
       "spaced-comment": [2, "always", { "exceptions": ["/"] }],
       "no-loop-func": 0, // this is more annoying than useful when properly using ES2015 block bindings
       "no-underscore-dangle": 0,
       "no-console": 0,
       "no-alert": 0,
       "no-shadow": 0
    }
}


================================================
FILE: .gitignore
================================================
dist/
node_modules/
npm-debug.log
.sass-cache/

# IDE/text editors
*.sublime-*
.project
.idea/
nbproject/


================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
node_js:
  - "0.10"
  - "0.12"
  - "iojs"
  - "4"
  - "5"


================================================
FILE: CHANGELOG.md
================================================
# v0.0.11 released this Mon 10, 2015
- Added Node.js 0.10, 0.12 and io.js support.
- `harmonic run` now opens the site in the default browser automatically, and `new_post`/`new_page` open the created markdown file(s) in the default editor automatically as well (unless the `--no-open` flag is passed).
- All `harmonic` commands now accept an optional project `path` argument, which (still) defaults to the CWD.
- Added Less preprocessor support to templates (this will be moved to a Harmonic plugin in the future).
- Added useful error message when running Harmonic commands in a non-Harmonic project directory.
- Added useful error message when running unrecognized Harmonic commands.

### Bug fixes
- Many fixes regarding Promises and race conditions.
- Fixed crash when posts/pages directory contains non-markdown files (e.g. `.DS_Store`).

### Internal
- Build: all Node.js `.js` files are now compiled with Babel.
- Build: Grunt.js -> gulp, based on the [slush-es20xx](https://github.com/JSRocksHQ/slush-es20xx) workflow.
- ES.next: all the CommonJS syntax has been replaced with ECMAScript Modules syntax.
- Development: fixed `npm link` in Unix-based OS's.
- CI: added unit tests.
- CI: use container-based infrastructure on Travis.
- Streamlining internal functions for soon-to-be-implemented API's consumption.
- ES.next: make use of many more ECMAScript 2015 features.
- Lots of general code refactoring and cleanup.

### Miscellaneous
- Docs: added [Contributing Guide](https://github.com/JSRocksHQ/harmonic/blob/master/CONTRIBUTING.md).
- Docs: updated the Wiki's [Installing page](https://github.com/JSRocksHQ/harmonic/wiki/Installing).
- Docs: revamped the repository's [README](https://github.com/JSRocksHQ/harmonic#the-next-static-site-generator) header.
- Added [Harmonic Gitter room](https://gitter.im/JSRocksHQ/harmonic).


# 0.0.9 released on Oct 10, 2014
- Fully multi-platform support (Linux, Mac, Windows)
- Removed old trash
- Better bootstrap with _init_ command
- i18n support
- Basic documentation
- Create page command
- Bind pages to the browser API
A special thanks to the awesome contributors for this release:  
@leobalter, @UltCombo, @rssilva, @soapdog


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing guide

Want to contribute to Harmonic? Awesome!
There are many ways you can contribute, see below.

## Opening issues

Open an issue to report bugs or to propose new features.

- Reporting bugs: describe the bug as clearly as you can, including steps to reproduce, what happened and what you were expecting to happen. Also include Node.js or browser version, OS and other related software's versions when applicable.

- Proposing features: explain the proposed feature, what it should do, why it is useful, how users should use it. Give us as much info as possible so it will be easier to discuss, access and implement the proposed feature. When you're unsure about a certain aspect of the feature, feel free to leave it open for others to discuss and find an appropriate solution.

## Proposing pull requests

Pull requests are very welcome. Note that if you are going to propose drastic changes, be sure to open an issue for discussion first, to make sure that your PR will be accepted before you spend effort coding it.

Fork the Harmonic repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.

`cd` to your Harmonic repository and run `npm link` to install dependencies, build and symlink your Harmonic repository to the globally installed npm packages. This means `npm link` will do all the work for you and make the `harmonic` command available in your terminal/command prompt.

To start working, `cd` to your Harmonic repository and run `npm run dev` to make a new build and (if the build succeeded) enter watch mode, which will generate incremental builds and run tests whenever you edit files.

Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.

## Documentation

Documentation is extremely important and takes a fair deal of time and effort to write and keep updated. Please submit any and all improvements you can make to the repository's docs and the [Wiki](https://github.com/JSRocksHQ/harmonic/wiki).


================================================
FILE: LICENSE
================================================
(The MIT License)

Copyright (c) 2014, Jaydson Gomes <jayalemao@gmail.com>, Átila Fassina <hey@atilafassina.com> and others contributors.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# <a name="harmonic" href="http://harmonicjs.com/"><img src="https://cdn.rawgit.com/JSRocksHQ/harmonic/e391ae462f3b047848f1783315de9edab019e197/harmonic-logo.svg" alt="Harmonic - The next static site generator" width="450"></a>
[![Build Status](https://travis-ci.org/JSRocksHQ/harmonic.svg?branch=master)](https://travis-ci.org/JSRocksHQ/harmonic)
[![Dependency Status](http://img.shields.io/david/JSRocksHQ/harmonic.svg)](https://david-dm.org/JSRocksHQ/harmonic)
[![devDependency Status](http://img.shields.io/david/dev/JSRocksHQ/harmonic.svg)](https://david-dm.org/JSRocksHQ/harmonic#info=devDependencies)
[![Gitter](https://img.shields.io/badge/gitter-join_chat-1dce73.svg)](https://gitter.im/JSRocksHQ/harmonic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

**Please note that this project is currently under development.**  
**Contributions are very welcome!**

Harmonic is being developed with some goals:  
- Learn and play with ECMAScript 2015 (ES6) and beyond (in node and the browser)
- Build a simple static site generator in node using ES2015+ features
- Create the JS Rocks website with Harmonic!  
(Actually, the website is already online: [JS Rocks](http://jsrocks.org/))  

Check out the full [Harmonic documentation](doc).

## Installing

Harmonic is available on npm:  

```shell
npm install harmonic -g
```
For more details, check out the full documentation: [Installing](doc/installing.md)

## Init
First thing you will need to do is to initialize a new Harmonic website.  
It is as simple as:  
```shell
harmonic init [PATH]
```
[PATH] is your website dir. The default path is the current dir.  
Harmonic will prompt you asking for some data about your website:   
![Config](doc/img/config.png)  

Harmonic will then generate a config file, which is a simple JSON object.  
Any time you want, you can configure your static website with the CLI `config` command:  
```shell
cd [PATH]
harmonic config
```
Now, enter in your website dir and you are ready to start [creating posts](#blogging)!  
For more details, check out the full documentation: [Config](doc/config.md)

## Blogging
Harmonic follows the same pattern as others static site generators that you may know.  
You must write your posts in [Markdown](http://daringfireball.net/projects/markdown/) format.  

### New post:  
```
cd your_awesome_website
harmonic new_post "Hello World"
```
![New Post](doc/img/new_post.png)

After running `new_post`, a markdown file will be generated in the `/src/posts/[lang]` folder, ready for editing.  

#### Markdown header
The markdown file have a header which defines the post metadata.  
Example:  
```markdown
<!--
layout: post
title: hello world
date: 2014-05-17T08:18:47.847Z
comments: true
published: true
keywords: JavaScript, ES2015
description: Hello world post
categories: JavaScript, ES2015
authorName: Jaydson
-->
```
You can check all possible header values in the [header page](doc/markdown-header.md).  

#### Markdown content
Everything after the header is the post content.  
Example:  
```markdown
# Hello World  
This is my awesome post using [Harmonic](https://github.com/JSRocksHQ/harmonic).  

This is a list:  
- Item 1
- Item 2
- Item 3
```
The code above will be parsed to something like this:  
```html
<h1 id="hello-world">Hello World</h1>
<p>
  This is my awesome post using 
  <a href="https://github.com/JSRocksHQ/harmonic">Harmonic</a>.
</p>
<p>This is a list:  </p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
```
For more details, you can check the full documentation: [Blogging](doc/blogging.md).  
## New Page
```
cd your_awesome_website
harmonic new_page "Hello World Page"
```
After running `new_page`, a markdown file will be generated in the `/src/pages/[lang]` folder, ready for editing.  

## Build
The build tool will generate the index page, posts, pages, categories, compile styles and ES2015+.
```shell
harmonic build
```

## Run
To run your static server:
```shell
harmonic run
```
You can specify a port, by default Harmonic will use the 9356 port:
```shell
harmonic run 9090
```

Harmonic will also watch all files in the `src` directory and in the currently selected theme, triggering a new build and reloading the opened pages when changes are detected.

## Help
```shell
harmonic --help
```

Also see the full [Harmonic documentation](doc).

## Contributing
See the [Contributing guide](CONTRIBUTING.md).


================================================
FILE: build.js
================================================
'use strict';

module.exports = {
    srcBase: 'src/',
    src: {
        js: ['**/*.js', '!bin/skeleton/**/*.js']
    },
    distBase: 'dist/',
    config: {
        babel: { optional: ['runtime'], stage: 0 },
        mocha: '--colors --bail --timeout 15000'
    }
};


================================================
FILE: doc/README.md
================================================
# Harmonic documentation

[Installing](installing.md)  
[Configuring](config.md)  
[Blogging](blogging.md)  
[Markdown Header](markdown-header.md)  
[Themes](themes.md)  


================================================
FILE: doc/blogging.md
================================================
# Blogging with Harmonic

Harmonic follow the pattern of others static site generators you may know.  
You must write your posts in [Markdown](http://daringfireball.net/projects/markdown/) format.  

## New post:  
```
harmonic new_post "Hello World"
```
![New Post](img/new_post.png)

After running **_new_post_**, the markdown file will be generated in _**/src/posts/**_ folder.  

### Markdown header
The markdown file have a header which defines the post meta-data.  
Example:  
```markdown
<!--
layout: post
title: hello world
date: 2014-05-17T08:18:47.847Z
comments: true
published: true
keywords: JavaScript, ES6
description: Hello world post
categories: JavaScript, ES6
authorName: Jaydson
-->
```
You can check all possible header values in the [header page](markdown-header.md).  

### Markdown content
Everything after the header is the post content.  
Example:  
```markdown
# Hello World  
This is my awesome post using [harmonic](https://github.com/es6rocks/harmonic).  

This is a list:  
- Item 1
- Item 2
- Item 3
```
The code above will be parsed to something like this:  
```html
<h1 id="hello-world">Hello World</h1>
<p>
  This is my awesome post using
  <a href="https://github.com/es6rocks/harmonic">harmonic</a>.
</p>
<p>This is a list:  </p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
```

[<<< Configuring Harmonic](config.md) | [Markdown Header >>>](markdown-header.md)


================================================
FILE: doc/config.md
================================================
# Configuring Harmonic

The Harmonic config file is a simple JSON object.  
You can configure your static website with the CLI _config_ command:  
```shell
harmonic config
```
![Config](img/config.png)

Feel free to open and change your harmonic config file, actually some of these configurations aren't available on the command line helper.  
So, let's check the full config file:  
<table>
  <tr>
    <td>
       <strong>key</strong>
    </td>
    <td>
       <strong>value</strong>
    </td>
    <td>
       <strong>example</strong>
    </td>
 </tr>
 <tr>
  <tr>
    <td>
       name
    </td>
    <td>
       The name of your website
    </td>
    <td>
       <i>My awesome blog</i>
    </td>
 </tr>
 <tr>
    <td>
       title
    </td>
    <td>
       The title of your webiste
    </td>
    <td>
       <i>My awesome title</i>
    </td>
 </tr>
 <tr>
    <td>
       domain
    </td>
    <td>
       The domain of your website - NOT IMPLEMENTED YET*
    </td>
    <td>
       <i>http://es6rocks.com</i>
    </td>
 </tr>
 <tr>
    <td>
       subtitle
    </td>
    <td>
       The subtitle of your website - NOT IMPLEMENTED YET*
    </td>
    <td>
       <i>My awesome subtitle</i>
    </td>
 </tr>
 <tr>
    <td>
       author
    </td>
    <td>
       Your name
    </td>
    <td>
       <i>John da Silva</i>
    </td>
 </tr>
 <tr>
    <td>
       keywords
    </td>
    <td>
       The keywords of the page or post - NOT IMPLEMENTED YET*
    </td>
    <td>
      <i>JavaScript, HTML5, CSS3</i>
    </td>
 </tr>
 <tr>
    <td>
       description
    </td>
    <td>
       Some description of your website
    </td>
    <td>
       <i>Just a description</i>
    </td>
 </tr>
 <tr>
    <td>
       template
    </td>
    <td>
       The template you choose to use
    </td>
    <td>
      <i>default</i>
    </td>
 </tr>
 <tr>
    <td>
       posts_permalink
    </td>
    <td>
       The posts permalink of your website
    </td>
    <td>
      <i>:year/:month/:title</i>
    </td>
  </tr>
 <tr>
    <td>
       pages_permalink
    </td>
    <td>
       The pages permalink of your website
    </td>
    <td>
       <i>pages/:title</i>
    </td>
  </tr>
</table>

All this information are available in any template as _**config**_.  

[<<< Installing Harmonic](installing.md) | [Blogging with Harmonic >>>](blogging.md)


================================================
FILE: doc/installing.md
================================================
# Installing Harmonic

## Prerequirements

- Node.js >= 0.10 or io.js.
- npm.

## Simple installation

Harmonic is available on npm:

```shell
npm install harmonic -g
```

## Bleeding edge installation

You can install Harmonic directly from the GitHub repository:

```shell
git clone https://github.com/es6rocks/harmonic.git
cd harmonic
npm link
```

If everything is ok, you can type `harmonic` in your terminal, and you will get the harmonic menu:  
![Harmonic CLI](img/harmonic-cli.png)

[<<< Index](README.md) | [Configuring Harmonic >>>](config.md)


================================================
FILE: doc/markdown-header.md
================================================
# Markdown header

The markdown header is the metadata for your post or page.  
Let's check all available options harmonic have:  
<table>
  <tr>
    <td>
       layout
    </td>
    <td>
       The layout of the file (post|page) - NOT IMPLEMENTED YET*
    </td>
 </tr>
 <tr>
    <td>
       title
    </td>
    <td>
       The title of your post or page
    </td>
 </tr>
 <tr>
    <td>
       date
    </td>
    <td>
       The date in JSON format (Ex: new Date().toJSON())
    </td>
 </tr>
 <tr>
    <td>
       comments
    </td>
    <td>
       If the page or post have comments enabled - NOT IMPLEMENTED YET*
    </td>
 </tr>
 <tr>
    <td>
       published
    </td>
    <td>
        If the page or post is available online - NOT IMPLEMENTED YET*
    </td>
 </tr>
 <tr>
    <td>
       keywords
    </td>
    <td>
       The keywords of the page or post - NOT IMPLEMENTED YET*
    </td>
 </tr>
 <tr>
    <td>
       description
    </td>
    <td>
       The post/page description
    </td>
 </tr>
 <tr>
    <td>
       categories
    </td>
    <td>
       The categories
    </td>
 </tr>
 <tr>
    <td>
       authorName
    </td>
    <td>
       The authorName
    </td>
  </tr>
</table>

NOT IMPLEMENTED YET means the default theme doesn't support those features.  

[<<< Blogging with Harmonic](blogging.md) | [Harmonic themes >>>](themes.md)


================================================
FILE: doc/themes.md
================================================
# Harmonic themes

_**Introduced in Harmonic@0.1.0**_

Harmonic themes are based on the awesome [Nunjucks](https://mozilla.github.io/nunjucks/).  
Basically, if you want to create a Harmonic theme, you can use all the Nunjucks features.  
Harmonic themes are [npm packages](https://www.npmjs.com/), meaning you can easily share and use community themes.

## How to create a Harmonic theme

### npm package

First, you'll need to create a `npm` package:

```bash
mkdir harmonic-theme-awesome
cd harmonic-theme-awesome
npm init
```

Configure your npm package the way you want.  
In the end, you'll have a `package.json`.

### Building your theme

Harmonic themes must implement 3 template files:

- index.html (theme main page)
- post.html (post page for a blog)
- page.html (for an page)

Also, you can create your own structure, like a `partials` directory with your html partials.  

index example:

```html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ config.title }}</title>
</head>
<body>

    {% include "partials/navigation.html" %}
    <header>
        <section>
            <h1>
                {{ config.title }}
            </h1>
        </section>
    </header>

{% include "partials/footer.html" %}

</body>
</html>
```

___Static files___ must be placed in a folder called `resources`.  
Everything inside this folder will be copied.  
You can also provide a `config.json` file that will be merged with the current Harmonic config.  
Example:

```json
{
    "mycustomdata": "wow",
    "foo": "bar",
    "baz": ["a", "b"]
}
```

Here's a sample theme structure (actually, the harmonic-theme-default uses this structure):

```
.
├── config.json
├── index.html
├── package.json
├── page.html
├── partials
│   ├── footer.html
│   ├── header.html
│   └── navigation.html
├── post.html
├── README.md
├── resources
│   ├── css
│   │   └── main.css
│   ├── images
│   │   ├── flag-en.jpg
│   │   └── flag-pt-br.jpg
│   └── js
│       └── main.js
└── tag_archives.html
```

## Using your theme

If you're developing a new theme, you will most likely want to test it locally before publishing it.  
To test your theme locally, you can just install it like any other npm package, but pointing to its path:

```bash
npm install ../harmonic-theme-awesome
```

Then edit your `harmonic.json` and set `"theme": "harmonic-theme-awesome"`.

Note: To install the theme you must first init a new Harmonic project, or use an existing one:

```bash
harmonic init "my_blog"
cd my_blog
npm install ../harmonic-theme-awesome
```

## Faster development

To avoid having to `npm install` your theme every time you make changes, you may instead [`npm link`](https://docs.npmjs.com/cli/link) your theme to a Harmonic project:

```bash
# suppose you have a Harmonic theme named `harmonic-theme-awesome` in this dir:
cd harmonic-theme-awesome
npm link
# and a Harmonic project here:
cd ../my_blog
npm link harmonic-theme-awesome
```

Now, your theme is symlinked to that Harmonic project, meaning any change you make to the theme will be automatically reflected in the Harmonic project's theme dependency (`node_modules/harmonic-theme-awesome`). Note that you still need to run `harmonic build` or `harmonic run` to generate a new site build using the newly modified theme to see it in action.

The next step in the theme development workflow would be to setup a watch task to run `harmonic build` and auto-reload the browser (e.g. using [BrowserSync](http://www.browsersync.io/)), but that is outside of the scope of this wiki page. `;)`

## Publish

If you'd like, add the `"harmonictheme"` [keyword](https://docs.npmjs.com/files/package.json#keywords) to your `package.json`, so that users may easily [find your theme](https://www.npmjs.com/search?q=harmonictheme).

As Harmonic themes are just npm packages, you can publish it like any other package.  
Assuming you already have npm configured (registered user, etc.):

```bash
npm publish ./
```

Now, everybody can use your theme!

```bash
harmonic init "my_blog"
cd my_blog
npm install harmonic-theme-awesome
```

[<<< Markdown Header](markdown-header.md) | [Index >>>](README.md)


================================================
FILE: entry_points/README.md
================================================
# Entry points

This directory exists to work around a `npm link` issue in Unix-based OS's -- if these files were inside `dist`, generating a new build would break the symlinks.

Put your entry point's logic in the `src/` directory.


================================================
FILE: entry_points/harmonic.js
================================================
#!/usr/bin/env node
'use strict';

require('babel-runtime/core-js').default.Promise = require('bluebird');
process.on('unhandledRejection', function(reason/*, promise*/) {
    console.log('Possibly Unhandled Rejection:');
    console.log(reason instanceof Error ? reason.stack || reason.toString() : reason);
});

module.exports = require('../dist/bin/cli/harmonic');


================================================
FILE: gulpfile.js
================================================
'use strict';

var path = require('path');
var exec = require('child_process').exec;
var gulp = require('gulp');
var plugins = require('gulp-load-plugins')();
var rimraf = require('rimraf');
var mergeStream = require('merge-stream');
var globManip = require('glob-manipulate');
var build = require('./build');
var copySrc = ['**'].concat(globManip.negate(build.src.js));

// Run unit tests in complete isolation, see https://github.com/JSRocksHQ/harmonic/issues/122#issuecomment-85333442
function runTests(opt, cb) {
	var child = exec('mocha ' + build.config.mocha + ' "' + build.distBase + 'test"', {
	  maxBuffer: 16 * 1024 * 1024,
	}, function(err/*, stdout, stderr*/) {
		cb(opt.ignoreErrors ? null : err);
	});

	child.stdout.pipe(process.stdout);
	child.stderr.pipe(process.stderr);
}

gulp.task('clean', function(cb) {
	rimraf(build.distBase, cb);
});

gulp.task('build', ['clean'], function(cb) {
	// [[gulp4]] TODO remove srcOrderedGlobs
	mergeStream(
		plugins.srcOrderedGlobs(globManip.prefix(build.src.js, build.srcBase), { base: build.srcBase })
			.pipe(plugins.eslint())
			.pipe(plugins.eslint.format())
			.pipe(plugins.eslint.failAfterError())
			.pipe(plugins.babel(build.config.babel))
			.on('error', function(err) {
				// Improve error logging:
				// workaround cmd.exe color issue, show timestamp and error type, hide call stack.
				plugins.util.log(err.toString());
				process.exit(1);
			}),
		plugins.srcOrderedGlobs(globManip.prefix(copySrc, build.srcBase), { base: build.srcBase })
	)
		.pipe(gulp.dest(build.distBase))
		.on('end', function() {
			runTests({ ignoreErrors: false }, cb);
		})
		.resume();
});

gulp.task('default', ['clean'], function(cb) {
	var vinylPaths = require('vinyl-paths');
	var streamify = require('stream-array');
	var through2 = require('through2');
	var chalk = require('chalk');

	var srcToDistRelativePath = path.relative(build.srcBase, build.distBase);
	var SIGINTed = false;

	var filesFailingLint = [];
	var filesFailingCompile = [];
	function addToFailingList(list, filePath) {
		if (list.indexOf(filePath) === -1) list.push(filePath);
	}
	function removeFromFailingList(list, filePath) {
		var idx = list.indexOf(filePath);
		if (idx !== -1) list.splice(idx, 1);
	}

	// Diagram reference: https://github.com/JSRocksHQ/slush-es20xx/issues/5#issue-52701608 // TODO update diagram
	var batched = batch(function(files, cb) {
		files = files
			.pipe(plugins.plumber(function(err) {
				if (err.plugin === 'gulp-babel') {
					addToFailingList(filesFailingCompile, err.fileName);
				}

				plugins.util.log(err.toString());
			}));

		var existingFiles = files
			.pipe(plugins.filter(function(file) {
				return file.event === 'change' || file.event === 'add';
			}));

		mergeStream(
			// js pipe
			existingFiles
				.pipe(plugins.filter(build.src.js))
				.pipe(plugins.eslint())
				.pipe(plugins.eslint.format())
				.pipe(through2.obj(function(file, enc, cb) {
					if (file.eslint && file.eslint.messages && file.eslint.messages.length) {
						addToFailingList(filesFailingLint, file.path);
					} else {
						removeFromFailingList(filesFailingLint, file.path);
					}
					cb(null, file);
				}))
				.pipe(plugins.babel(build.config.babel))
				.pipe(through2.obj(function(file, enc, cb) {
					removeFromFailingList(filesFailingCompile, file.path);
					cb(null, file);
				}))
				.pipe(gulp.dest(build.distBase)),

			// copy pipe
			existingFiles
				.pipe(plugins.filter(copySrc))
				.pipe(gulp.dest(build.distBase)),

			// deletion pipe
			files
				.pipe(plugins.filter(function(file) {
					return file.event === 'unlink';
				}))
				.pipe(through2.obj(function(file, enc, cb) {
					removeFromFailingList(filesFailingLint, file.path);
					removeFromFailingList(filesFailingCompile, file.path);
					cb(null, file);
				}))
				.pipe(plugins.rename(function(filePath) {
					// we can't change/remove the filePath's `base`, so cd out of it in the dirname
					filePath.dirname = path.join(srcToDistRelativePath, filePath.dirname);
				}))
				.pipe(vinylPaths(rimraf))
		)
			.on('end', function() {
				if (filesFailingCompile.length) {
					var plural = filesFailingCompile.length !== 1;
					plugins.util.log(
						chalk.yellow((plural ? 'These files are' : 'This file is') + ' failing compilation:\n')
						+ chalk.red(filesFailingCompile.join('\n'))
						+ chalk.yellow('\nSkipping unit tests until ' + (plural ? 'these files are' : 'this file is') + ' fixed.')
					);
					endBatch();
					return;
				}

				runTests({ ignoreErrors: true }, endBatch);

				function endBatch() {
					if (filesFailingLint.length) {
						plugins.util.log(
							chalk.yellow((filesFailingLint.length !== 1 ? 'These files have' : 'This file has') + ' linting issues:\n')
							+ chalk.red(filesFailingLint.join('\n'))
						);
					}

					cb(); // must call batch's cb before checking `batched.isActive()`
					plugins.util.log(
						chalk.green('Batch completed.')
						+ (!SIGINTed && !batched.isActive() ? ' Watching ' + chalk.magenta(build.srcBase) + ' directory for changes...' : '')
					);
					maybeEndTask();
				}
			})
			.resume();
	});

	var watchStream = plugins.watch(build.srcBase + '**', { base: build.srcBase, ignoreInitial: false }, batched)
		.on('ready', function() {
			plugins.util.log('Watching ' + chalk.magenta(build.srcBase) + ' directory for changes...');
		})
		.on('end', maybeEndTask);

	var rl;
	if (process.platform === 'win32') {
		rl = require('readline').createInterface({
			input: process.stdin,
			output: process.stdout,
		}).on('SIGINT', function() {
			process.emit('SIGINT');
		});
	}

	process.on('SIGINT', function() {
		if (SIGINTed) return;
		SIGINTed = true;
		watchStream.close();
	});

	function maybeEndTask() {
		if (!SIGINTed || batched.isActive()) return;
		if (rl) rl.close();
		cb();
		process.exit(0);
	}

	// Simplified fork of gulp-batch, with removed domains (async-done) and added most recent unique('path') deduping logic.
	// Added isActive() method which returns whether the callback is currently executing or if there are any batched/queued files waiting for execution.
	function batch(cb) {

		var batch = [];
		var isRunning = false;
		var timeout;
		var delay = 100; // ms

		function setupFlushTimeout() {
			if (!isRunning && batch.length) {
				timeout = setTimeout(flush, delay);
			}
		}

		function flush() {
			isRunning = true;
			cb(streamify(batch), function() {
				isRunning = false;
				setupFlushTimeout();
			});
			batch = [];
		}

		function doBatch(newFile) {
			if (!batch.some(function(file, idx) {
				if (newFile.path === file.path) {
					batch[idx] = newFile;
					return true;
				}
			})) batch.push(newFile);

			clearTimeout(timeout);
			setupFlushTimeout();
		};

		doBatch.isActive = function() {
			return isRunning || !!batch.length;
		};

		return doBatch;
	};
});


================================================
FILE: package.json
================================================
{
  "name": "harmonic",
  "description": "The next static site generator",
  "homepage": "https://github.com/JSRocksHQ/harmonic",
  "version": "0.5.1",
  "engines": {
    "node": ">=0.10"
  },
  "preferGlobal": true,
  "bin": {
    "harmonic": "entry_points/harmonic.js"
  },
  "files": [
    "dist",
    "!dist/test",
    "entry_points",
    "doc"
  ],
  "repository": {
    "type": "git",
    "url": "git://github.com/JSRocksHQ/harmonic.git"
  },
  "keywords": [
    "ssg",
    "generator",
    "blog",
    "scaffold",
    "markdown",
    "website",
    "static",
    "static generator",
    "static site generator",
    "ES6",
    "ES7",
    "ES2015",
    "ES2016"
  ],
  "dependencies": {
    "babel-runtime": "5.8.34",
    "bluebird": "^3.1.1",
    "browser-sync": "^2.11.0",
    "chokidar": "^1.4.2",
    "cli-color": "^1.1.0",
    "co": "^3.1.0",
    "co-prompt": "^1.0.0",
    "commander": "^2.9.0",
    "core-js": "^2.0.3",
    "dedent": "^0.6.0",
    "less": "^2.5.3",
    "marked-metadata": "0.0.6",
    "mkdirp": "^0.5.1",
    "ncp": "^2.0.0",
    "npm": "^3.5.3",
    "nunjucks": "^2.3.0",
    "open": "0.0.5",
    "permalinks": "^0.3.1",
    "pretty-ms": "^2.1.0",
    "rimraf": "^2.4.1",
    "stylus": "^0.53.0"
  },
  "devDependencies": {
    "babel-eslint": "^5.0.0-beta6",
    "chalk": "^1.1.1",
    "glob-manipulate": "^1.1.1",
    "gulp": "^3.9.0",
    "gulp-babel": "^5.3.0",
    "gulp-eslint": "^1.1.1",
    "gulp-filter": "^3.0.1",
    "gulp-load-plugins": "^1.2.0",
    "gulp-plumber": "^1.0.1",
    "gulp-rename": "^1.2.2",
    "gulp-src-ordered-globs": "^1.0.3",
    "gulp-util": "^3.0.7",
    "gulp-watch": "^4.3.5",
    "merge-stream": "^1.0.0",
    "mocha": "^2.3.4",
    "should": "^8.1.1",
    "stream-array": "^1.1.1",
    "through2": "^2.0.0",
    "vinyl-paths": "^1.0.0"
  },
  "author": "Jaydson Gomes <jayalemao@gmail.com> (http://jaydson.org/)",
  "license": "MIT",
  "scripts": {
    "dev": "gulp",
    "test": "gulp build",
    "update-babel": "npm install --save --save-exact babel-runtime@5 && npm update --depth=1 babel-core",
    "require-clean-work-tree": "(git update-index -q --ignore-submodules --refresh && git diff-files --quiet --ignore-submodules && git diff-index --cached --quiet --ignore-submodules HEAD --) || (echo You have uncommitted changes. Please commit or stash them. >&2 && exit 1)",
    "preversion": "git pull && npm run --silent require-clean-work-tree && npm run --silent update-babel && (git diff-files --quiet -- package.json || git commit -m \"update Babel\" -- package.json) && npm test",
    "postversion": "git push --follow-tags && npm publish"
  }
}


================================================
FILE: src/bin/cli/harmonic.js
================================================
/* eslint-disable no-process-exit */

import program from 'commander';
import { version } from '../config';
import { cliColor } from '../helpers';
import logo from './logo';
import { init, config, newFile, run } from './util';
import { build } from '../core';
const clc = cliColor();

program
    .version(version);

program
    .command('init [path]')
    .description('Init your static website')
    .action((path = '.') => {
        console.log(logo);
        init(path);
    });

program
    .command('config [path]')
    .description('Config your static website')
    .action(async (path = '.') => {
        console.log(logo);
        await config(path);
        console.log(clc.info('\nharmonic.json successfully updated.'));
    });

program
    .command('build [path]')
    .description('Build your static website')
    .action((path = '.') => {
        build(path);
    });

program
    .command('new_post <title> [path]')
    .option('--no-open', 'Don\'t open the markdown file(s) in editor')
    .description('Create a new post')
    .action((title, path = '.', { open: autoOpen }) => {
        newFile(path, 'post', title, autoOpen);
    });

program
    .command('new_page <title> [path]')
    .option('--no-open', 'Don\'t open the markdown file(s) in editor')
    .description('Create a new page')
    .action((title, path = '.', { open: autoOpen }) => {
        newFile(path, 'page', title, autoOpen);
    });

program
    .command('run [port] [path]')
    .option('--no-open', 'Don\'t open a new browser window')
    .description('Run your static site locally. Port is optional')
    .action(async (port = 9356, path = '.', { open: autoOpen }) => {
        await build(path);
        run(path, port, autoOpen);
    });

program.on('*', (args) => {
    console.error('Unknown command: ' + clc.error(args[0]));
    process.exit(1);
});

program.parse(process.argv);

// Not enough arguments
if (!program.args.length) {
    console.log(logo);
    program.help();
}


================================================
FILE: src/bin/cli/logo.js
================================================
import { version } from '../config';
import { cliColor } from '../helpers';

const clc = cliColor();
const logo = clc.message(`
|_| _  _ _ _  _  _ . _
| |(_|| | | |(_)| ||(_
                 ${version}
`);

export default logo;


================================================
FILE: src/bin/cli/util.js
================================================
import fs from 'fs';
import path from 'path';
import { promisify, promisifyAll, fromNode as promiseFromNode } from 'bluebird';
import chokidar from 'chokidar';
import browserSync from 'browser-sync';
import co from 'co';
import prompt from 'co-prompt';
import mkdirp from 'mkdirp';
import { ncp } from 'ncp';
import open from 'open';
import { load as npmLoad } from 'npm';
import dd from 'dedent';
import { rootdir, postspath, pagespath } from '../config';
import { cliColor, getConfig, titleToFilename, findHarmonicRoot, displayNonInitializedFolderErrorMessage, MissingFileError } from '../helpers';
import { build } from '../core';
promisifyAll(fs);
const npmLoadAsync = promisify(npmLoad);
const mkdirpAsync = promisify(mkdirp);
const ncpAsync = promisify(ncp);
const clc = cliColor();

export { init, config, newFile, run, openFile };

// Open a file using browser, text-editor
function openFile(type, sitePath, file) {
    if (type === 'file') {
        open(path.resolve(sitePath, file));
    } else {
        // TODO unless we add a --no-watch flag, we can just delegate this to browser-sync
        open(file);
    }
}

function config(passedPath, _skipFindRoot = false) {
    const sitePath = _skipFindRoot ? passedPath : findHarmonicRoot(passedPath);

    if (!sitePath) {
        displayNonInitializedFolderErrorMessage();
        throw new MissingFileError();
    }

    const manifest = path.join(sitePath, 'harmonic.json');

    return new Promise((fulfill, reject) => {
        co(function*() {
            console.log(clc.message(
                'This guide will help you to create your Harmonic configuration file\n' +
                'Just hit enter if you are ok with the default values.\n\n'
            ));

            /* eslint-disable camelcase */
            const templateObj = {
                name: 'Awesome website',
                title: 'My awesome static website',
                domain: 'http://awesome.com',
                subtitle: 'Powered by Harmonic',
                author: 'Jaydson',
                description: 'This is the description',
                bio: 'Thats me',
                theme: 'harmonic-theme-default',
                preprocessor: 'stylus',
                posts_permalink: ':language/:year/:month/:title',
                pages_permalink: ':language/pages/:title',
                header_tokens: ['<!--', '-->'],
                index_posts: 10,
                i18n: {
                    default: 'en',
                    languages: ['en', 'pt-br']
                }
            };
            /* eslint-enable camelcase */

            function _p(message) {
                return prompt(clc.message(message));
            }

            const config = {
                name: (yield _p('Site name: (' + templateObj.name + ') ')) ||
                    templateObj.name,
                title: (yield _p('Title: (' + templateObj.title + ') ')) ||
                    templateObj.title,
                subtitle: (yield _p('Subtitle: (' + templateObj.subtitle + ') ')) ||
                    templateObj.subtitle,
                description: (yield _p('Description: (' + templateObj.description + ') ')) ||
                    templateObj.description,
                author: (yield _p('Author: (' + templateObj.author + ') ')) ||
                    templateObj.author,
                bio: (yield _p('Author bio: (' + templateObj.bio + ') ')) ||
                    templateObj.bio,
                theme: (yield _p('Theme: (' + templateObj.theme + ') ')) ||
                    templateObj.theme,
                preprocessor: (yield _p('Preprocessor: (' + templateObj.preprocessor + ') ')) ||
                    templateObj.preprocessor
            };

            process.stdin.pause();

            // create the configuration file
            fs.writeFile(manifest, JSON.stringify(Object.assign({}, templateObj, config), null, 4), (err) => {
                if (err) {
                    reject(err);
                    return;
                }
                fulfill();
            });
        })();
    });
}

async function init(sitePath) {
    const skeletonPath = path.join(rootdir, 'bin/skeleton');

    await mkdirpAsync(sitePath);
    await ncpAsync(skeletonPath, sitePath, { stopOnErr: true });
    console.log(clc.message('Harmonic skeleton started at: ' + path.resolve(sitePath)));

    await config(sitePath, true);

    console.log(clc.info('\nInstalling dependencies...'));
    const npm = await npmLoadAsync();
    try {
        await promisify(npm.commands.install)(sitePath, []);
    } catch (e) {
        console.error(dd
            `Command ${clc.error('npm install')} failed.
             Make sure you are connected to the internet and execute the command above in your Harmonic skeleton directory.`
        );
    }

    console.log('\n' + clc.info(dd
        `Your Harmonic website skeleton was successfully created!
         Now, browse the project directory and have fun.`
    ));
}

/**
 * @param {string} type - The new file's type. Can be either 'post' or 'page'.
 * @param {string} title - The new file's title.
 */
function newFile(passedPath, type, title, autoOpen) {
    const sitePath = findHarmonicRoot(passedPath);

    if (!sitePath) {
        displayNonInitializedFolderErrorMessage();
        throw new MissingFileError();
    }

    const langs = getConfig(sitePath).i18n.languages;
    // TODO use template literal + dedent
    const template = '<!--\n' +
        'layout: ' + type + '\n' +
        'title: ' + title + '\n' +
        'date: ' + new Date().toJSON() + '\n' +
        'comments: true\n' +
        'published: true\n' +
        'keywords:\n' +
        'description:\n' +
        'categories:\n' +
        '-->\n' +
        '# ' + title;
    const filedir = path.join(sitePath, type === 'post' ? postspath : pagespath);
    const filename = titleToFilename(title);

    langs.forEach((lang) => {
        const fileLangDir = path.join(filedir, lang);
        const fileW = path.join(fileLangDir, filename);
        mkdirp.sync(fileLangDir);
        fs.writeFileSync(fileW, template);
        if (autoOpen) {
            openFile('text', sitePath, fileW);
        }
    });

    console.log(clc.info(
        type[0].toUpperCase() + type.slice(1) +
        ' "' + title + '" was successefuly created, check your /src/' + type + 's folder'
    ));
}

async function run(passedPath, port, autoOpen) {
    const sitePath = findHarmonicRoot(passedPath);

    if (!sitePath) {
        displayNonInitializedFolderErrorMessage();
        throw new MissingFileError();
    }

    let isBuilding = false;
    let pendingBuild = false;
    const bs = browserSync.create();

    await promiseFromNode((cb) => {
        bs.init({
            server: {
                baseDir: path.join(sitePath, 'public'),
                middleware: [
                    (req, res, next) => {
                        if (isBuilding) {
                            // TODO deliver browser-sync snippet when requesting a html file
                            res.writeHead(503);
                            res.end();
                            return;
                        }
                        next();
                    }
                ]
            },
            port,
            open: autoOpen
        }, cb);
    });

    console.log(clc.info(`Harmonic site is running.`));

    async function buildSite() {
        try {
            await build(sitePath);
            if (!pendingBuild) {
                bs.reload();
            }
        } catch (err) {
            console.error(clc.error('Build error:'));
            console.error(err.stack || err.toString());
        }

        if (pendingBuild) {
            pendingBuild = false;
            await buildSite();
        }
    }

    async function doBuildSite() {
        // TODO watcher.unwatch and watcher.add when selected theme changes in harmonic.json
        if (isBuilding) {
            pendingBuild = true;
            return;
        }

        isBuilding = true;
        await buildSite();
        isBuilding = false;
    }

    const watcher = chokidar.watch(['src', 'harmonic.json', path.join('node_modules', getConfig(sitePath).theme)], {
        // interval: 1000,
        // atomic: false,
        ignoreInitial: true,
        // ignorePermissionErrors: true,
        cwd: sitePath
    })
        .on('add', doBuildSite)
        // .on('addDir', doBuildSite)
        .on('change', doBuildSite)
        .on('unlink', doBuildSite)
        // .on('unlinkDir', doBuildSite)
        .on('error', (error) => console.error('Error occurred', error));

    await promiseFromNode((cb) => watcher.on('ready', cb));
    console.log(clc.info('Watching Harmonic project for changes...'));

    return { watcher };
}


================================================
FILE: src/bin/client/.babelrc
================================================
{
  "blacklist": ["runtime"]
}


================================================
FILE: src/bin/client/harmonic-client.js
================================================
/* exported Harmonic */
/* global __HARMONIC */

// Note: `__HARMONIC` is not an actual identifer,
// it is the prefix of `harmonic build`'s substitution patterns.
// The substitution patterns look like a property access so that
// we can just whitelist `__HARMONIC` as a global identifier
// instead of having to whitelist every single substitution.

// TODO ESLint's `exported` directive seems to not be working correctly
// with the current version.
// We should probably `export` Harmonic using ES2015 module syntax and
// trash the `exported` directive.
class Harmonic { // eslint-disable-line no-unused-vars

    constructor(name) {
        this.name = name;
    }

    getConfig() {
        return __HARMONIC.CONFIG__;
    }

    getPosts() {
        return __HARMONIC.POSTS__;
    }

    getPages() {
        return __HARMONIC.PAGES__;
    }
}


================================================
FILE: src/bin/config.js
================================================
import { normalize, join } from 'path';

// rootdir === `dist` dir
export const rootdir = normalize(join(__dirname, '/../'));
export { version } from '../../package.json';
export const postspath = normalize('./src/posts/');
export const pagespath = normalize('./src/pages/');


================================================
FILE: src/bin/core.js
================================================
import prettyMs from 'pretty-ms';
import { findHarmonicRoot, displayNonInitializedFolderErrorMessage, MissingFileError } from './helpers';
import Harmonic from './parser';
import { cliColor } from './helpers';
const clc = cliColor();

export { build };

async function build(passedPath) {
    const startTime = Date.now();

    const sitePath = findHarmonicRoot(passedPath);

    if (!sitePath) {
        displayNonInitializedFolderErrorMessage();
        throw new MissingFileError();
    }

    const harmonic = new Harmonic(sitePath, { quiet: false });

    await harmonic.clean();

    const postsDataPromise = (async () => await harmonic.generateFiles(await harmonic.getPostFiles(), 'post'))();
    const pagesDataPromise = (async () => await harmonic.generateFiles(await harmonic.getPageFiles(), 'page'))();

    await Promise.all([
        harmonic.compileCSS(),
        (async () => await harmonic.generateIndex(await postsDataPromise, await pagesDataPromise))(),
        (async () => await harmonic.generateTagsPages(await postsDataPromise))(),
        (async () => await harmonic.compileJS(await postsDataPromise, await pagesDataPromise))(),
        (async () => await harmonic.generateRSS(await postsDataPromise, await pagesDataPromise))(),
        (async () => {
            // finish copying theme resources first to allow user resources to overwrite them.
            await harmonic.copyThemeResources();
            await harmonic.copyUserResources();
        })()
    ]);

    // TODO move logging to outside of this API?
    const endTime = Date.now();
    console.log(clc.info(`Build completed in ${prettyMs(endTime - startTime)}.`));
}


================================================
FILE: src/bin/helpers.js
================================================
import { readFileSync } from 'fs';
import { join, resolve, extname, basename } from 'path';
import strIncludes from 'core-js/library/fn/string/virtual/includes';
import _cliColor from 'cli-color';

export { cliColor, isHarmonicProject, getConfig, titleToFilename,
    findHarmonicRoot, displayNonInitializedFolderErrorMessage, getFileName, getStructure };

// CLI color
function cliColor() {
    return {
        info: _cliColor.green,
        error: _cliColor.red,
        warn: _cliColor.yellowBright,
        message: _cliColor.yellow
    };
}

// Friendly message for non-initialized folder
function displayNonInitializedFolderErrorMessage() {
    const clc = cliColor();

    console.log(
        clc.warn('It seems this is not an Harmonic project yet. \n') +
        clc.warn('Check your directory or run ') +
        clc.info.bgWhite.italic(' harmonic init ') +
        clc.warn(' to start a new Harmonic project.')
    );
}

// Check if harmonic.json file exists
function isHarmonicProject(sitePath) {
    try {
        getConfig(sitePath);
        return true;
    } catch (e) {
        return false;
    }
}

// Find harmonic.json. Returns path or false.
function findHarmonicRoot(sitePath) {
    let currentPath = resolve(sitePath);
    let oldPath = '';

    // Climb directories up until finding a `harmonic.json` file, if it is not found then return false.
    while (!isHarmonicProject(currentPath)) {
        oldPath = currentPath;
        currentPath = resolve(currentPath, '..');

        if (oldPath === currentPath) {
            // reached root folder, return false;
            return false;
        }
    }

    return currentPath;
}

function getConfig(sitePath) {
    return JSON.parse(readFileSync(join(sitePath, 'harmonic.json')).toString());
}

function titleToFilename(title) {
    return title.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() + '.md';
}

function getFileName(file) {
    const filename = basename(file, extname(file));
    const checkDate = new Date(filename.substr(0, 10));
    return isNaN(checkDate.getDate()) ? filename : filename.substr(11, filename.length);
}

function getStructure(defaultLang, lang, permaLink) {
    // If is the default language, generate in the root path
    if (defaultLang === lang && permaLink::strIncludes(':language')) {
        // TODO allow customizing the permalink format? https://github.com/JSRocksHQ/harmonic/pull/97#issuecomment-67596545
        return permaLink.split(':language/')[1];
    }
    return permaLink;
}

// Note: class declarations are not hoisted, so this can't be listed in the exports statement at the top of this file.
export class MissingFileError extends Error {
    constructor(file = 'harmonic.json') {
        super();
        this.name = 'MissingFileError';
        this.file = file;
        this.message = `Missing file: ${this.file}`;
        delete this.stack;
    }
}


================================================
FILE: src/bin/parser.js
================================================
import path from 'path';
import fs from 'fs';
import padStart from 'core-js/library/fn/string/virtual/pad-start';
import { promisify, promisifyAll, fromNode as promiseFromNode } from 'bluebird';
import nunjucks from 'nunjucks';
import permalinks from 'permalinks';
import MkMeta from 'marked-metadata';
import mkdirp from 'mkdirp';
import { ncp } from 'ncp';
import rimraf from 'rimraf';
import stylus from 'stylus';
import less from 'less';
import { rootdir, postspath, pagespath } from './config';
import { cliColor, getConfig, getFileName, getStructure } from './helpers';
import Theme from './theme';
promisifyAll(fs);
const mkdirpAsync = promisify(mkdirp);
const ncpAsync = promisify(ncp);
const rimrafAsync = promisify(rimraf);

const clc = cliColor();
const rMarkdownExt = /\.(?:md|markdown)$/;

export default class Harmonic {
    /* eslint-disable camelcase */

    constructor(sitePath, { quiet = true } = {}) {
        this.sitePath = path.resolve(sitePath);
        this.quiet = !!quiet;

        const config = Object.assign({
            index_posts: 10
        }, getConfig(this.sitePath));
        this.theme = new Theme(config.theme, this.sitePath);

        if (fs.existsSync(path.join(this.theme.themePath, 'config.json'))) {
            Object.assign(config, JSON.parse(this.theme.getFileContents('config.json')));
        }

        this.config = config;
        this.nunjucksEnv = nunjucks.configure(this.theme.themePath);
        this.templates = {};
    }

    async clean() {
        console.log(clc.warn('Cleaning up...'));
        await rimrafAsync(path.join(this.sitePath, 'public'), { maxBusyTries: 20 });
    }

    async compileCSS() {
        const currentCSSCompiler = this.config.preprocessor;
        if (!currentCSSCompiler) {
            return;
        }

        const compiler = {

            less: async () => {
                const lessIndexPath = path.join(this.theme.themePath, 'resources/_less/index.less');
                const cssDir = path.join(this.sitePath, 'public/css');

                const lessInput = await fs.readFileAsync(lessIndexPath, { encoding: 'utf8' });
                const { css } = await less.render(lessInput, { filename: lessIndexPath });

                await mkdirpAsync(cssDir);
                await fs.writeFileAsync(path.join(cssDir, 'main.css'), css);

                console.log(clc.info('Successfully generated CSS with LESS preprocessor'));
            },

            stylus: async () => {
                const stylDir = path.join(this.theme.themePath, 'resources/_stylus');
                const stylIndexPath = path.join(stylDir, 'index.styl');
                const cssDir = path.join(this.sitePath, 'public/css');

                const stylInput = await fs.readFileAsync(stylIndexPath, { encoding: 'utf8' });
                const css = await promiseFromNode((cb) => {
                    stylus(stylInput)
                        .set('filename', stylIndexPath)
                        .set('paths', [path.join(stylDir, 'engine'), path.join(stylDir, 'partials')])
                        .render(cb);
                });

                await mkdirpAsync(cssDir);
                await fs.writeFileAsync(path.join(cssDir, 'main.css'), css);

                console.log(clc.info('Successfully generated CSS with Stylus preprocessor'));
            }
        };

        if (!compiler.hasOwnProperty(currentCSSCompiler)) {
            throw new Error(`Unsupported CSS preprocessor: ${currentCSSCompiler}`);
        }

        await compiler[currentCSSCompiler]();
    }

    async compileJS(postsMetadata, pagesMetadata) {
        const harmonicClient = (await fs.readFileAsync(`${rootdir}/bin/client/harmonic-client.js`, { encoding: 'utf8' }))
            .replace(/__HARMONIC\.POSTS__/g, JSON.stringify(postsMetadata))
            .replace(/__HARMONIC\.PAGES__/g, JSON.stringify(pagesMetadata))
            .replace(/__HARMONIC\.CONFIG__/g, JSON.stringify(this.config));

        await fs.writeFileAsync(path.join(this.sitePath, 'public/harmonic.js'), harmonicClient);
    }

    async generateTagsPages(postsMetadata) {
        const tagTemplateNJ = nunjucks.compile(this.theme.getFileContents('index.html'), this.nunjucksEnv);
        const config = this.config;

        await Promise.all([].concat(...Object.entries(postsMetadata).map(([lang, langPosts]) => {
            const postsByTag = {};
            langPosts.forEach((post) => {
                post.categories.forEach((category) => {
                    // TODO replace with kebabCase?
                    const tag = category.toLowerCase().trim().split(' ').join('-');
                    postsByTag[tag] = postsByTag[tag] || [];
                    postsByTag[tag].push(post);
                });
            });

            return Object.entries(postsByTag).map(async ([tag, tagPosts]) => {
                const tagContent = tagTemplateNJ.render({
                    posts: tagPosts,
                    config,
                    category: tag,
                    lang
                });

                const tagPath = path.join(this.sitePath, 'public/categories', ...(config.i18n.default === lang ? [] : [lang]), tag);
                await mkdirpAsync(tagPath);
                await fs.writeFileAsync(path.join(tagPath, 'index.html'), tagContent);
                console.log(clc.info(`Successfully generated tag[${tag}] archive html file`));
            });
        })));
    }

    async generateIndex(postsMetadata, pagesMetadata) {
        const indexTemplateNJ = nunjucks.compile(this.theme.getFileContents('index.html'), this.nunjucksEnv);
        const config = this.config;

        await Promise.all(Object.entries(postsMetadata).map(async ([lang, langPosts]) => {
            const posts = langPosts.slice(0, config.index_posts);
            const pages = pagesMetadata[lang] || [];

            const indexContent = indexTemplateNJ.render({
                posts,
                pages,
                config,
                lang
            });

            const indexPath = path.join(this.sitePath, 'public', ...(config.i18n.default === lang ? [] : [lang]));
            await mkdirpAsync(indexPath);
            await fs.writeFileAsync(path.join(indexPath, 'index.html'), indexContent);
            console.log(clc.info(`${lang}/index file successfully created`));
        }));
    }

    async copyThemeResources() {
        await ncpAsync(path.join(this.theme.themePath, 'resources'), path.join(this.sitePath, 'public'), { stopOnErr: true });
        console.log(clc.info('Theme resources copied'));
    }

    async copyUserResources() {
        const userResourcesPath = path.join(this.sitePath, 'resources');
        await mkdirpAsync(userResourcesPath);
        await ncpAsync(userResourcesPath, path.join(this.sitePath, 'public'), { stopOnErr: true });
        console.log(clc.info(`User resources copied`));
    }

    getTemplate(layout) {
        if(!this.templates[layout]) {
            const templateContents = this.theme.getFileContents(`${layout}.html`);
            this.templates[layout] = nunjucks.compile(templateContents, this.nunjucksEnv);
        }
        return this.templates[layout];
    }

    async generateFiles(files, fileType) {
        const langs = Object.keys(files);
        const config = this.config;
        const generatedFiles = {};
        const currentDate = new Date();
        const tokens = config.header_tokens || ['<!--', '-->'];
        const metadataDefaults = {
            layout: fileType
        };

        const filesPath = fileType === 'post' ? postspath : pagespath;

        await Promise.all([].concat(...langs.map((lang) => files[lang].map(async (file) => {
            const md = new MkMeta(path.join(this.sitePath, filesPath, lang, file));
            md.defineTokens(tokens[0], tokens[1]);

            const metadata = this.normalizeMetaData(md.metadata(), metadataDefaults);
            metadata.content = new nunjucks.runtime.SafeString(md.markdown({
                crop: '<!--more-->'
            }));

            const template = this.getTemplate(metadata.layout);
            const filename = getFileName(file);
            const permalink = fileType === 'post' ? config.posts_permalink : config.pages_permalink;

            const filePath = permalinks({
                replacements: [{
                    pattern: ':year',
                    replacement: metadata.date.getFullYear()
                }, {
                    pattern: ':month',
                    replacement: (metadata.date.getMonth() + 1)::padStart(2, '0')
                }, {
                    pattern: ':title',
                    replacement: filename
                }, {
                    pattern: ':language',
                    replacement: lang
                }],
                structure: getStructure(config.i18n.default, lang, permalink)
            });

            metadata.file = filesPath + file;
            metadata.filename = filename;
            metadata.link = filePath;
            metadata.lang = lang;

            const contentHTMLFile = template
                .render({
                    [fileType]: {
                        content: new nunjucks.runtime.SafeString(md.markdown()),
                        metadata
                    },
                    config,
                    lang
                })
                .replace(/<!--[\s\S]*?-->/g, '');

            if(fileType === 'page') {
                metadata.content = new nunjucks.runtime.SafeString(contentHTMLFile);
            }

            if (metadata.published && metadata.published === 'false') {
                return;
            }

            if (metadata.date && metadata.date > currentDate) {
                console.log(clc.info(`Skipping future ${fileType} ${metadata.filename}`));
                return;
            }

            const publicFileDirPath = path.join(this.sitePath, 'public', filePath);
            const publicFilePath = path.join(publicFileDirPath, 'index.html');
            await mkdirpAsync(publicFileDirPath);

            await fs.writeFileAsync(publicFilePath, contentHTMLFile);
            console.log(clc.info(`Successfully generated ${fileType} ${filePath}`));

            generatedFiles[lang] = generatedFiles[lang] || [];
            generatedFiles[lang].push(metadata);
        }))));

        return fileType === 'post' ? this.sortByDate(generatedFiles) : this.sortByName(generatedFiles);
    }

    async getPostFiles() {
        const files = {};

        await Promise.all(this.config.i18n.languages.map(async (lang) => {
            files[lang] = (await fs.readdirAsync(path.join(this.sitePath, postspath, lang)))
                .filter((p) => rMarkdownExt.test(p));
        }));

        return files;
    }

    async getPageFiles() {
        const files = {};

        await Promise.all(this.config.i18n.languages.map(async (lang) => {
            const langPath = path.join(this.sitePath, pagespath, lang);
            await mkdirpAsync(langPath);
            files[lang] = (await fs.readdirAsync(langPath)).filter((p) => rMarkdownExt.test(p));
        }));

        return files;
    }

    async generateRSS(postsMetadata, pagesMetadata) {
        const rssTemplate = await fs.readFileAsync(path.join(__dirname, 'resources/rss.xml'), { encoding: 'utf8' });
        const rssTemplateNJ = nunjucks.compile(rssTemplate, this.nunjucksEnv);
        const config = this.config;
        const rssAuthor = config.author_email ? `${config.author_email} ( ${config.author} )` : config.author;

        await Promise.all(Object.entries(postsMetadata).map(async ([lang, langPosts]) => {
            const posts = langPosts.slice(0, config.index_posts);
            const isDefaultLang = config.i18n.default === lang;
            const rssPath = path.join(this.sitePath, 'public', ...(isDefaultLang ? [] : [lang]));
            const rssLink = `${config.domain}${isDefaultLang ? '' : '/' + lang}/rss.xml`;

            const rssContent = rssTemplateNJ.render({
                rss: {
                    date: new Date().toUTCString(),
                    link: rssLink,
                    author: rssAuthor,
                    lang: lang
                },
                posts,
                config,
                pages: pagesMetadata
            });

            await mkdirpAsync(rssPath);
            await fs.writeFileAsync(`${rssPath}/rss.xml`, rssContent);
            console.log(clc.info(`${lang}/rss.xml file successfully created`));
        }));
    }

    sortByDate(files) {
        Object.values(files).forEach((filesArray) => filesArray.sort((a, b) => new Date(b.date) - new Date(a.date)));
        return files;
    }

    sortByName(files) {
        Object.values(files).forEach((filesArray) => filesArray.sort(
            (a, b) => a.filename.toLowerCase() > b.filename.toLowerCase() ? 1 : -1
        ));
        return files;
    }

    normalizeMetaData(data, defaults) {
        data.categories = (data.categories || '').split(',').map((category) => category.trim());

        if (data.date) {
            data.date = new Date(data.date);
        }

        data.layout = data.layout || defaults.layout;

        return data;
    }

}


================================================
FILE: src/bin/resources/rss.xml
================================================
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>{{ config.title }}</title>
        <link>{{ config.domain }}</link>
        <description>{{ config.description }}</description>
        <managingEditor>{{ rss.author }}</managingEditor>
        <webMaster>{{ rss.author }}</webMaster>
        <pubDate>{{ rss.date }}</pubDate>
        <language>{{ rss.lang }}</language>
        <atom:link href="{{rss.link}}" rel="self" type="application/rss+xml" />
        {% for post in posts %}
        <item>
            <title>{{ post.title}}</title>
            <link>{{config.domain}}/{{post.link}}</link>
            <description><![CDATA[{{post.content}}]]></description>
            <pubDate>{{post.date.toUTCString()}}</pubDate>
            <guid>{{config.domain}}/{{post.link}}</guid>
        </item>
        {% endfor %}
    </channel>
</rss>


================================================
FILE: src/bin/skeleton/package.json
================================================
{
  "private": true,
  "dependencies": {
    "harmonic-theme-default": "latest"
  }
}


================================================
FILE: src/bin/skeleton/resources/readme.txt
================================================
Add files to this folder to have them copied to the final site during build time.

PS: You can remove this file.


================================================
FILE: src/bin/skeleton/src/pages/en/about.md
================================================
<!--
layout: page
title: About
date: 2014-05-27T07:18:47.847Z
comments: false
published: true
keywords: JavaScript, ES6
description: About page
categories:
authorName: Jaydson
-->
# Hello page

================================================
FILE: src/bin/skeleton/src/pages/pt-br/about.md
================================================
<!--
layout: page
title: About
date: 2014-05-27T07:18:47.847Z
comments: false
published: true
keywords: JavaScript, ES6
description: About page
categories:
authorName: Jaydson
-->
# Olá página

================================================
FILE: src/bin/skeleton/src/posts/en/hello-world.md
================================================
<!--
layout: post
title: hello world
date: 2014-05-17T08:18:47.847Z
comments: true
published: true
keywords: JavaScript, ES6
description: Hello world post
categories: JavaScript, ES6
authorName: Jaydson
-->
Hello World!


================================================
FILE: src/bin/skeleton/src/posts/pt-br/hello-world.md
================================================
<!--
layout: post
title: ola mundo
date: 2014-05-17T08:18:47.847Z
comments: true
published: true
keywords: JavaScript, ES6
description: Hello world post
categories: JavaScript, ES6
authorName: Jaydson
-->
Olá mundo!


================================================
FILE: src/bin/theme.js
================================================
import { resolve, join } from 'path';
import { readFileSync } from 'fs';
import dd from 'dedent';

export default class Theme {

    constructor(name, sitePath) {
        if (!name) {
            throw new Error('Invalid theme. Please check your harmonic.json file.');
        }

        this.name = name;
        this.sitePath = resolve(sitePath);
        this.themePath = join(this.sitePath, 'node_modules', name);
    }

    getFileContents(file) {
        try {
            return readFileSync(join(this.themePath, file), { encoding: 'utf8' });
        } catch (e) {
            throw new Error(dd
                `Harmonic failed to load a theme file: "${file}".
                 Please check your selected theme in the harmonic.json, make sure it is correctly installed and has all the necessary files.`
            );
        }
    }
}


================================================
FILE: src/test/main.js
================================================
/* eslint-env mocha */

import { spawn, exec } from 'child_process';
import { join } from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { sync as rimrafSync } from 'rimraf';
import { sync as mkdirpSync } from 'mkdirp';
import 'should';
import Harmonic from '../bin/parser';
import { isHarmonicProject, getConfig, titleToFilename } from '../bin/helpers';
import { postspath, pagespath } from '../bin/config';

const testDir = join(__dirname, 'site');
const harmonicBin = join(__dirname, '../../entry_points/harmonic');
const stdoutWrite = process.stdout.write;

before(() => {
    rimrafSync(testDir);
    mkdirpSync(testDir);
});

after(() => {
    rimrafSync(testDir);
});

function disableStdout() {
    process.stdout.write = () => {};
}
function enableStdout() {
    process.stdout.write = stdoutWrite;
}

describe('CLI', () => {
    it('should display an error for unknown commands', (done) => {
        exec('node "' + harmonicBin + '" foobarbaz', (error, stdout, stderr) => {
            error.code.should.equal(1);
            stderr.should.containEql('foobarbaz');
            done();
        });
    });

    it('should init a new Harmonic site', (done) => {
        const harmonic = spawn('node', [harmonicBin, 'init', testDir]);
        harmonic.stdin.setEncoding('utf8');
        harmonic.stdout.setEncoding('utf8');

        harmonic.stdout.on('data', (data) => {
            if (data.indexOf('successfully created') === -1) {
                harmonic.stdin.write('\n');
                return;
            }
            harmonic.stdin.end();
        });

        harmonic.on('close', () => {
            isHarmonicProject(testDir).should.be.true();
            done();
        });
    });

    it('should build the Harmonic site', (done) => {
        const harmonic = spawn('node', [harmonicBin, 'build', testDir]);
        harmonic.stdin.setEncoding('utf8');
        harmonic.stdout.setEncoding('utf8');

        harmonic.on('close', () => {
            existsSync(join(testDir, 'public')).should.be.true();
            done();
        });
    });

    it('should create and build a new post', async () => {
        const config = getConfig(testDir);
        const langs = config.i18n.languages;
        const title = 'new_post test';
        const fileName = titleToFilename(title);
        const harmonic = spawn('node', [harmonicBin, 'new_post', '--no-open', title, testDir]);
        harmonic.stdin.setEncoding('utf8');
        harmonic.stdout.setEncoding('utf8');

        await new Promise((resolve) => {
            harmonic.on('close', () => {
                for (const lang of langs) {
                    readFileSync(
                        join(testDir, postspath, lang, fileName)
                    ).toString().should.containEql(title);
                }
                resolve();
            });
        });

        const harmonicBuild = spawn('node', [harmonicBin, 'build', testDir]);
        harmonicBuild.stdin.setEncoding('utf8');
        harmonicBuild.stdout.setEncoding('utf8');
        await new Promise((resolve) => {
            harmonicBuild.on('close', () => {
                const date = new Date(),
                    year = String(date.getFullYear()),
                    month = ('0' + (date.getMonth() + 1)).slice(-2),
                    slug = fileName.replace(/\.md$/, '');
                for (const lang of langs) {
                    const langSegment = lang === config.i18n.default ? '.' : lang;
                    readFileSync(join(testDir, 'public', langSegment, year, month,
                        slug, 'index.html')).toString().should.containEql(`<h1 id="new_post-test">${title}</h1>`);
                }
                resolve();
            });
        });
    });

    it('should create and build a new page', async () => {
        const config = getConfig(testDir);
        const langs = config.i18n.languages;
        const title = 'new_page test';
        const fileName = titleToFilename(title);
        const harmonic = spawn('node', [harmonicBin, 'new_page', '--no-open', title, testDir]);
        harmonic.stdin.setEncoding('utf8');
        harmonic.stdout.setEncoding('utf8');

        await new Promise((resolve) => {
            harmonic.on('close', () => {
                for (const lang of langs) {
                    readFileSync(
                        join(testDir, pagespath, lang, fileName)
                    ).toString().should.containEql(title);
                }
                resolve();
            });
        });

        const harmonicBuild = spawn('node', [harmonicBin, 'build', testDir]);
        harmonicBuild.stdin.setEncoding('utf8');
        harmonicBuild.stdout.setEncoding('utf8');
        await new Promise((resolve) => {
            harmonicBuild.on('close', () => {
                const slug = fileName.replace(/\.md$/, '');
                for (const lang of langs) {
                    const langSegment = lang === config.i18n.default ? '.' : lang;
                    readFileSync(join(testDir, 'public', langSegment, 'pages',
                        slug, 'index.html')).toString().should.containEql(`<h1 id="new_page-test">${title}</h1>`);
                }
                resolve();
            });
        });
    });
});

describe('helpers', () => {
    it('.isHarmonicProject() should return whether the CWD is a Harmonic site', () => {
        disableStdout();
        const result = isHarmonicProject(__dirname);
        enableStdout();
        result.should.be.false();
        isHarmonicProject(testDir).should.be.true();
    });

    it('.titleToFilename() should transform a post/page title into a filename', () => {
        titleToFilename('Hello World!').should.equal('hello-world.md');
    });
});

describe('API', () => {
    it('should merge the theme\'s config into the main config', () => {
        const config = getConfig(testDir);
        const themeConfigPath = join(testDir, 'node_modules', config.theme, 'config.json');
        const templateConfig = { customData: 'test' };
        writeFileSync(themeConfigPath, JSON.stringify(templateConfig));

        const harmonic = new Harmonic(testDir);
        const mergedConfig = harmonic.config;

        mergedConfig.should.containDeep(templateConfig);
        mergedConfig.should.eql(Object.assign({}, config, templateConfig));
    });
});
Download .txt
gitextract_a6js28w6/

├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.js
├── doc/
│   ├── README.md
│   ├── blogging.md
│   ├── config.md
│   ├── installing.md
│   ├── markdown-header.md
│   └── themes.md
├── entry_points/
│   ├── README.md
│   └── harmonic.js
├── gulpfile.js
├── package.json
└── src/
    ├── bin/
    │   ├── cli/
    │   │   ├── harmonic.js
    │   │   ├── logo.js
    │   │   └── util.js
    │   ├── client/
    │   │   ├── .babelrc
    │   │   └── harmonic-client.js
    │   ├── config.js
    │   ├── core.js
    │   ├── helpers.js
    │   ├── parser.js
    │   ├── resources/
    │   │   └── rss.xml
    │   ├── skeleton/
    │   │   ├── package.json
    │   │   ├── resources/
    │   │   │   └── readme.txt
    │   │   └── src/
    │   │       ├── pages/
    │   │       │   ├── en/
    │   │       │   │   └── about.md
    │   │       │   └── pt-br/
    │   │       │       └── about.md
    │   │       └── posts/
    │   │           ├── en/
    │   │           │   └── hello-world.md
    │   │           └── pt-br/
    │   │               └── hello-world.md
    │   └── theme.js
    └── test/
        └── main.js
Download .txt
SYMBOL INDEX (49 symbols across 8 files)

FILE: gulpfile.js
  function runTests (line 14) | function runTests(opt, cb) {
  function addToFailingList (line 63) | function addToFailingList(list, filePath) {
  function removeFromFailingList (line 66) | function removeFromFailingList(list, filePath) {
  function endBatch (line 143) | function endBatch() {
  function maybeEndTask (line 184) | function maybeEndTask() {
  function batch (line 193) | function batch(cb) {

FILE: src/bin/cli/util.js
  function openFile (line 25) | function openFile(type, sitePath, file) {
  function config (line 34) | function config(passedPath, _skipFindRoot = false) {
  function init (line 110) | async function init(sitePath) {
  function newFile (line 140) | function newFile(passedPath, type, title, autoOpen) {
  function run (line 180) | async function run(passedPath, port, autoOpen) {

FILE: src/bin/client/harmonic-client.js
  class Harmonic (line 14) | class Harmonic { // eslint-disable-line no-unused-vars
    method constructor (line 16) | constructor(name) {
    method getConfig (line 20) | getConfig() {
    method getPosts (line 24) | getPosts() {
    method getPages (line 28) | getPages() {

FILE: src/bin/core.js
  function build (line 9) | async function build(passedPath) {

FILE: src/bin/helpers.js
  function cliColor (line 10) | function cliColor() {
  function displayNonInitializedFolderErrorMessage (line 20) | function displayNonInitializedFolderErrorMessage() {
  function isHarmonicProject (line 32) | function isHarmonicProject(sitePath) {
  function findHarmonicRoot (line 42) | function findHarmonicRoot(sitePath) {
  function getConfig (line 60) | function getConfig(sitePath) {
  function titleToFilename (line 64) | function titleToFilename(title) {
  function getFileName (line 68) | function getFileName(file) {
  function getStructure (line 74) | function getStructure(defaultLang, lang, permaLink) {
  class MissingFileError (line 84) | class MissingFileError extends Error {
    method constructor (line 85) | constructor(file = 'harmonic.json') {

FILE: src/bin/parser.js
  class Harmonic (line 24) | class Harmonic {
    method constructor (line 27) | constructor(sitePath, { quiet = true } = {}) {
    method clean (line 45) | async clean() {
    method compileCSS (line 50) | async compileCSS() {
    method compileJS (line 98) | async compileJS(postsMetadata, pagesMetadata) {
    method generateTagsPages (line 107) | async generateTagsPages(postsMetadata) {
    method generateIndex (line 138) | async generateIndex(postsMetadata, pagesMetadata) {
    method copyThemeResources (line 160) | async copyThemeResources() {
    method copyUserResources (line 165) | async copyUserResources() {
    method getTemplate (line 172) | getTemplate(layout) {
    method generateFiles (line 180) | async generateFiles(files, fileType) {
    method getPostFiles (line 265) | async getPostFiles() {
    method getPageFiles (line 276) | async getPageFiles() {
    method generateRSS (line 288) | async generateRSS(postsMetadata, pagesMetadata) {
    method sortByDate (line 318) | sortByDate(files) {
    method sortByName (line 323) | sortByName(files) {
    method normalizeMetaData (line 330) | normalizeMetaData(data, defaults) {

FILE: src/bin/theme.js
  class Theme (line 5) | class Theme {
    method constructor (line 7) | constructor(name, sitePath) {
    method getFileContents (line 17) | getFileContents(file) {

FILE: src/test/main.js
  function disableStdout (line 26) | function disableStdout() {
  function enableStdout (line 29) | function enableStdout() {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (76K chars).
[
  {
    "path": ".editorconfig",
    "chars": 241,
    "preview": "# editorconfig.org - unify code style\n# plugins for text editors: editorconfig.org/#download\nroot = true\n\n[*]\nindent_sty"
  },
  {
    "path": ".eslintrc",
    "chars": 886,
    "preview": "{\n    \"extends\": \"eslint:recommended\",\n    \"parser\": \"babel-eslint\",\n    \"env\": {\n        \"node\": true\n    },\n    \"rules"
  },
  {
    "path": ".gitignore",
    "chars": 106,
    "preview": "dist/\nnode_modules/\nnpm-debug.log\n.sass-cache/\n\n# IDE/text editors\n*.sublime-*\n.project\n.idea/\nnbproject/\n"
  },
  {
    "path": ".travis.yml",
    "chars": 88,
    "preview": "sudo: false\nlanguage: node_js\nnode_js:\n  - \"0.10\"\n  - \"0.12\"\n  - \"iojs\"\n  - \"4\"\n  - \"5\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2186,
    "preview": "# v0.0.11 released this Mon 10, 2015\n- Added Node.js 0.10, 0.12 and io.js support.\n- `harmonic run` now opens the site i"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2305,
    "preview": "# Contributing guide\n\nWant to contribute to Harmonic? Awesome!\nThere are many ways you can contribute, see below.\n\n## Op"
  },
  {
    "path": "LICENSE",
    "chars": 1162,
    "preview": "(The MIT License)\n\nCopyright (c) 2014, Jaydson Gomes <jayalemao@gmail.com>, Átila Fassina <hey@atilafassina.com> and oth"
  },
  {
    "path": "README.md",
    "chars": 4417,
    "preview": "# <a name=\"harmonic\" href=\"http://harmonicjs.com/\"><img src=\"https://cdn.rawgit.com/JSRocksHQ/harmonic/e391ae462f3b04784"
  },
  {
    "path": "build.js",
    "chars": 269,
    "preview": "'use strict';\n\nmodule.exports = {\n    srcBase: 'src/',\n    src: {\n        js: ['**/*.js', '!bin/skeleton/**/*.js']\n    }"
  },
  {
    "path": "doc/README.md",
    "chars": 171,
    "preview": "# Harmonic documentation\n\n[Installing](installing.md)  \n[Configuring](config.md)  \n[Blogging](blogging.md)  \n[Markdown H"
  },
  {
    "path": "doc/blogging.md",
    "chars": 1412,
    "preview": "# Blogging with Harmonic\n\nHarmonic follow the pattern of others static site generators you may know.  \nYou must write yo"
  },
  {
    "path": "doc/config.md",
    "chars": 2327,
    "preview": "# Configuring Harmonic\n\nThe Harmonic config file is a simple JSON object.  \nYou can configure your static website with t"
  },
  {
    "path": "doc/installing.md",
    "chars": 555,
    "preview": "# Installing Harmonic\n\n## Prerequirements\n\n- Node.js >= 0.10 or io.js.\n- npm.\n\n## Simple installation\n\nHarmonic is avail"
  },
  {
    "path": "doc/markdown-header.md",
    "chars": 1352,
    "preview": "# Markdown header\n\nThe markdown header is the metadata for your post or page.  \nLet's check all available options harmon"
  },
  {
    "path": "doc/themes.md",
    "chars": 4158,
    "preview": "# Harmonic themes\n\n_**Introduced in Harmonic@0.1.0**_\n\nHarmonic themes are based on the awesome [Nunjucks](https://mozil"
  },
  {
    "path": "entry_points/README.md",
    "chars": 233,
    "preview": "# Entry points\n\nThis directory exists to work around a `npm link` issue in Unix-based OS's -- if these files were inside"
  },
  {
    "path": "entry_points/harmonic.js",
    "chars": 368,
    "preview": "#!/usr/bin/env node\n'use strict';\n\nrequire('babel-runtime/core-js').default.Promise = require('bluebird');\nprocess.on('u"
  },
  {
    "path": "gulpfile.js",
    "chars": 6855,
    "preview": "'use strict';\n\nvar path = require('path');\nvar exec = require('child_process').exec;\nvar gulp = require('gulp');\nvar plu"
  },
  {
    "path": "package.json",
    "chars": 2625,
    "preview": "{\n  \"name\": \"harmonic\",\n  \"description\": \"The next static site generator\",\n  \"homepage\": \"https://github.com/JSRocksHQ/h"
  },
  {
    "path": "src/bin/cli/harmonic.js",
    "chars": 1978,
    "preview": "/* eslint-disable no-process-exit */\n\nimport program from 'commander';\nimport { version } from '../config';\nimport { cli"
  },
  {
    "path": "src/bin/cli/logo.js",
    "chars": 228,
    "preview": "import { version } from '../config';\nimport { cliColor } from '../helpers';\n\nconst clc = cliColor();\nconst logo = clc.me"
  },
  {
    "path": "src/bin/cli/util.js",
    "chars": 8804,
    "preview": "import fs from 'fs';\nimport path from 'path';\nimport { promisify, promisifyAll, fromNode as promiseFromNode } from 'blue"
  },
  {
    "path": "src/bin/client/.babelrc",
    "chars": 31,
    "preview": "{\n  \"blacklist\": [\"runtime\"]\n}\n"
  },
  {
    "path": "src/bin/client/harmonic-client.js",
    "chars": 852,
    "preview": "/* exported Harmonic */\n/* global __HARMONIC */\n\n// Note: `__HARMONIC` is not an actual identifer,\n// it is the prefix o"
  },
  {
    "path": "src/bin/config.js",
    "chars": 276,
    "preview": "import { normalize, join } from 'path';\n\n// rootdir === `dist` dir\nexport const rootdir = normalize(join(__dirname, '/.."
  },
  {
    "path": "src/bin/core.js",
    "chars": 1655,
    "preview": "import prettyMs from 'pretty-ms';\nimport { findHarmonicRoot, displayNonInitializedFolderErrorMessage, MissingFileError }"
  },
  {
    "path": "src/bin/helpers.js",
    "chars": 2900,
    "preview": "import { readFileSync } from 'fs';\nimport { join, resolve, extname, basename } from 'path';\nimport strIncludes from 'cor"
  },
  {
    "path": "src/bin/parser.js",
    "chars": 13255,
    "preview": "import path from 'path';\nimport fs from 'fs';\nimport padStart from 'core-js/library/fn/string/virtual/pad-start';\nimport"
  },
  {
    "path": "src/bin/resources/rss.xml",
    "chars": 902,
    "preview": "<?xml version=\"1.0\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n    <channel>\n        <title>{{ confi"
  },
  {
    "path": "src/bin/skeleton/package.json",
    "chars": 86,
    "preview": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"harmonic-theme-default\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "src/bin/skeleton/resources/readme.txt",
    "chars": 113,
    "preview": "Add files to this folder to have them copied to the final site during build time.\n\nPS: You can remove this file.\n"
  },
  {
    "path": "src/bin/skeleton/src/pages/en/about.md",
    "chars": 192,
    "preview": "<!--\nlayout: page\ntitle: About\ndate: 2014-05-27T07:18:47.847Z\ncomments: false\npublished: true\nkeywords: JavaScript, ES6\n"
  },
  {
    "path": "src/bin/skeleton/src/pages/pt-br/about.md",
    "chars": 192,
    "preview": "<!--\nlayout: page\ntitle: About\ndate: 2014-05-27T07:18:47.847Z\ncomments: false\npublished: true\nkeywords: JavaScript, ES6\n"
  },
  {
    "path": "src/bin/skeleton/src/posts/en/hello-world.md",
    "chars": 220,
    "preview": "<!--\nlayout: post\ntitle: hello world\ndate: 2014-05-17T08:18:47.847Z\ncomments: true\npublished: true\nkeywords: JavaScript,"
  },
  {
    "path": "src/bin/skeleton/src/posts/pt-br/hello-world.md",
    "chars": 216,
    "preview": "<!--\nlayout: post\ntitle: ola mundo\ndate: 2014-05-17T08:18:47.847Z\ncomments: true\npublished: true\nkeywords: JavaScript, E"
  },
  {
    "path": "src/bin/theme.js",
    "chars": 843,
    "preview": "import { resolve, join } from 'path';\nimport { readFileSync } from 'fs';\nimport dd from 'dedent';\n\nexport default class "
  },
  {
    "path": "src/test/main.js",
    "chars": 6365,
    "preview": "/* eslint-env mocha */\n\nimport { spawn, exec } from 'child_process';\nimport { join } from 'path';\nimport { existsSync, r"
  }
]

About this extraction

This page contains the full source code of the JSRocksHQ/harmonic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (69.2 KB), approximately 18.5k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!