├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.js ├── doc ├── README.md ├── blogging.md ├── config.md ├── img │ ├── config.png │ ├── harmonic-cli.png │ └── new_post.png ├── installing.md ├── markdown-header.md └── themes.md ├── entry_points ├── README.md └── harmonic.js ├── gulpfile.js ├── harmonic-logo.old.png ├── harmonic-logo.svg ├── 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 /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org - unify code style 2 | # plugins for text editors: editorconfig.org/#download 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = false 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "node": true 6 | }, 7 | "rules": { 8 | "strict": [2, "never"], // ES Modules are implicitly strict 9 | "no-use-before-define": [2, "nofunc"], // allow referencing function declarations in the whole scope where they're defined 10 | "no-var": 2, 11 | "curly": 2, 12 | "default-case": 2, 13 | "no-else-return": 2, 14 | "no-param-reassign": 2, 15 | "indent": [2, 4], 16 | "brace-style": 2, 17 | "comma-style": 2, 18 | "no-multiple-empty-lines": [2, { "max": 2 }], 19 | "quotes": [2, "single"], 20 | "spaced-comment": [2, "always", { "exceptions": ["/"] }], 21 | "no-loop-func": 0, // this is more annoying than useful when properly using ES2015 block bindings 22 | "no-underscore-dangle": 0, 23 | "no-console": 0, 24 | "no-alert": 0, 25 | "no-shadow": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | npm-debug.log 4 | .sass-cache/ 5 | 6 | # IDE/text editors 7 | *.sublime-* 8 | .project 9 | .idea/ 10 | nbproject/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "iojs" 7 | - "4" 8 | - "5" 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.11 released this Mon 10, 2015 2 | - Added Node.js 0.10, 0.12 and io.js support. 3 | - `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). 4 | - All `harmonic` commands now accept an optional project `path` argument, which (still) defaults to the CWD. 5 | - Added Less preprocessor support to templates (this will be moved to a Harmonic plugin in the future). 6 | - Added useful error message when running Harmonic commands in a non-Harmonic project directory. 7 | - Added useful error message when running unrecognized Harmonic commands. 8 | 9 | ### Bug fixes 10 | - Many fixes regarding Promises and race conditions. 11 | - Fixed crash when posts/pages directory contains non-markdown files (e.g. `.DS_Store`). 12 | 13 | ### Internal 14 | - Build: all Node.js `.js` files are now compiled with Babel. 15 | - Build: Grunt.js -> gulp, based on the [slush-es20xx](https://github.com/JSRocksHQ/slush-es20xx) workflow. 16 | - ES.next: all the CommonJS syntax has been replaced with ECMAScript Modules syntax. 17 | - Development: fixed `npm link` in Unix-based OS's. 18 | - CI: added unit tests. 19 | - CI: use container-based infrastructure on Travis. 20 | - Streamlining internal functions for soon-to-be-implemented API's consumption. 21 | - ES.next: make use of many more ECMAScript 2015 features. 22 | - Lots of general code refactoring and cleanup. 23 | 24 | ### Miscellaneous 25 | - Docs: added [Contributing Guide](https://github.com/JSRocksHQ/harmonic/blob/master/CONTRIBUTING.md). 26 | - Docs: updated the Wiki's [Installing page](https://github.com/JSRocksHQ/harmonic/wiki/Installing). 27 | - Docs: revamped the repository's [README](https://github.com/JSRocksHQ/harmonic#the-next-static-site-generator) header. 28 | - Added [Harmonic Gitter room](https://gitter.im/JSRocksHQ/harmonic). 29 | 30 | 31 | # 0.0.9 released on Oct 10, 2014 32 | - Fully multi-platform support (Linux, Mac, Windows) 33 | - Removed old trash 34 | - Better bootstrap with _init_ command 35 | - i18n support 36 | - Basic documentation 37 | - Create page command 38 | - Bind pages to the browser API 39 | A special thanks to the awesome contributors for this release: 40 | @leobalter, @UltCombo, @rssilva, @soapdog 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Want to contribute to Harmonic? Awesome! 4 | There are many ways you can contribute, see below. 5 | 6 | ## Opening issues 7 | 8 | Open an issue to report bugs or to propose new features. 9 | 10 | - 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. 11 | 12 | - 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. 13 | 14 | ## Proposing pull requests 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | `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. 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | ## Documentation 27 | 28 | 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). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014, Jaydson Gomes , Átila Fassina and others contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harmonic - The next static site generator 2 | [![Build Status](https://travis-ci.org/JSRocksHQ/harmonic.svg?branch=master)](https://travis-ci.org/JSRocksHQ/harmonic) 3 | [![Dependency Status](http://img.shields.io/david/JSRocksHQ/harmonic.svg)](https://david-dm.org/JSRocksHQ/harmonic) 4 | [![devDependency Status](http://img.shields.io/david/dev/JSRocksHQ/harmonic.svg)](https://david-dm.org/JSRocksHQ/harmonic#info=devDependencies) 5 | [![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) 6 | 7 | **Please note that this project is currently under development.** 8 | **Contributions are very welcome!** 9 | 10 | Harmonic is being developed with some goals: 11 | - Learn and play with ECMAScript 2015 (ES6) and beyond (in node and the browser) 12 | - Build a simple static site generator in node using ES2015+ features 13 | - Create the JS Rocks website with Harmonic! 14 | (Actually, the website is already online: [JS Rocks](http://jsrocks.org/)) 15 | 16 | Check out the full [Harmonic documentation](doc). 17 | 18 | ## Installing 19 | 20 | Harmonic is available on npm: 21 | 22 | ```shell 23 | npm install harmonic -g 24 | ``` 25 | For more details, check out the full documentation: [Installing](doc/installing.md) 26 | 27 | ## Init 28 | First thing you will need to do is to initialize a new Harmonic website. 29 | It is as simple as: 30 | ```shell 31 | harmonic init [PATH] 32 | ``` 33 | [PATH] is your website dir. The default path is the current dir. 34 | Harmonic will prompt you asking for some data about your website: 35 | ![Config](doc/img/config.png) 36 | 37 | Harmonic will then generate a config file, which is a simple JSON object. 38 | Any time you want, you can configure your static website with the CLI `config` command: 39 | ```shell 40 | cd [PATH] 41 | harmonic config 42 | ``` 43 | Now, enter in your website dir and you are ready to start [creating posts](#blogging)! 44 | For more details, check out the full documentation: [Config](doc/config.md) 45 | 46 | ## Blogging 47 | Harmonic follows the same pattern as others static site generators that you may know. 48 | You must write your posts in [Markdown](http://daringfireball.net/projects/markdown/) format. 49 | 50 | ### New post: 51 | ``` 52 | cd your_awesome_website 53 | harmonic new_post "Hello World" 54 | ``` 55 | ![New Post](doc/img/new_post.png) 56 | 57 | After running `new_post`, a markdown file will be generated in the `/src/posts/[lang]` folder, ready for editing. 58 | 59 | #### Markdown header 60 | The markdown file have a header which defines the post metadata. 61 | Example: 62 | ```markdown 63 | 74 | ``` 75 | You can check all possible header values in the [header page](doc/markdown-header.md). 76 | 77 | #### Markdown content 78 | Everything after the header is the post content. 79 | Example: 80 | ```markdown 81 | # Hello World 82 | This is my awesome post using [Harmonic](https://github.com/JSRocksHQ/harmonic). 83 | 84 | This is a list: 85 | - Item 1 86 | - Item 2 87 | - Item 3 88 | ``` 89 | The code above will be parsed to something like this: 90 | ```html 91 |

Hello World

92 |

93 | This is my awesome post using 94 | Harmonic. 95 |

96 |

This is a list:

97 | 102 | ``` 103 | For more details, you can check the full documentation: [Blogging](doc/blogging.md). 104 | ## New Page 105 | ``` 106 | cd your_awesome_website 107 | harmonic new_page "Hello World Page" 108 | ``` 109 | After running `new_page`, a markdown file will be generated in the `/src/pages/[lang]` folder, ready for editing. 110 | 111 | ## Build 112 | The build tool will generate the index page, posts, pages, categories, compile styles and ES2015+. 113 | ```shell 114 | harmonic build 115 | ``` 116 | 117 | ## Run 118 | To run your static server: 119 | ```shell 120 | harmonic run 121 | ``` 122 | You can specify a port, by default Harmonic will use the 9356 port: 123 | ```shell 124 | harmonic run 9090 125 | ``` 126 | 127 | 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. 128 | 129 | ## Help 130 | ```shell 131 | harmonic --help 132 | ``` 133 | 134 | Also see the full [Harmonic documentation](doc). 135 | 136 | ## Contributing 137 | See the [Contributing guide](CONTRIBUTING.md). 138 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | srcBase: 'src/', 5 | src: { 6 | js: ['**/*.js', '!bin/skeleton/**/*.js'] 7 | }, 8 | distBase: 'dist/', 9 | config: { 10 | babel: { optional: ['runtime'], stage: 0 }, 11 | mocha: '--colors --bail --timeout 15000' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Harmonic documentation 2 | 3 | [Installing](installing.md) 4 | [Configuring](config.md) 5 | [Blogging](blogging.md) 6 | [Markdown Header](markdown-header.md) 7 | [Themes](themes.md) 8 | -------------------------------------------------------------------------------- /doc/blogging.md: -------------------------------------------------------------------------------- 1 | # Blogging with Harmonic 2 | 3 | Harmonic follow the pattern of others static site generators you may know. 4 | You must write your posts in [Markdown](http://daringfireball.net/projects/markdown/) format. 5 | 6 | ## New post: 7 | ``` 8 | harmonic new_post "Hello World" 9 | ``` 10 | ![New Post](img/new_post.png) 11 | 12 | After running **_new_post_**, the markdown file will be generated in _**/src/posts/**_ folder. 13 | 14 | ### Markdown header 15 | The markdown file have a header which defines the post meta-data. 16 | Example: 17 | ```markdown 18 | 29 | ``` 30 | You can check all possible header values in the [header page](markdown-header.md). 31 | 32 | ### Markdown content 33 | Everything after the header is the post content. 34 | Example: 35 | ```markdown 36 | # Hello World 37 | This is my awesome post using [harmonic](https://github.com/es6rocks/harmonic). 38 | 39 | This is a list: 40 | - Item 1 41 | - Item 2 42 | - Item 3 43 | ``` 44 | The code above will be parsed to something like this: 45 | ```html 46 |

Hello World

47 |

48 | This is my awesome post using 49 | harmonic. 50 |

51 |

This is a list:

52 | 57 | ``` 58 | 59 | [<<< Configuring Harmonic](config.md) | [Markdown Header >>>](markdown-header.md) 60 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | # Configuring Harmonic 2 | 3 | The Harmonic config file is a simple JSON object. 4 | You can configure your static website with the CLI _config_ command: 5 | ```shell 6 | harmonic config 7 | ``` 8 | ![Config](img/config.png) 9 | 10 | Feel free to open and change your harmonic config file, actually some of these configurations aren't available on the command line helper. 11 | So, let's check the full config file: 12 | 13 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | 29 | 32 | 35 | 36 | 37 | 40 | 43 | 46 | 47 | 48 | 51 | 54 | 57 | 58 | 59 | 62 | 65 | 68 | 69 | 70 | 73 | 76 | 79 | 80 | 81 | 84 | 87 | 90 | 91 | 92 | 95 | 98 | 101 | 102 | 103 | 106 | 109 | 112 | 113 | 114 | 117 | 120 | 123 | 124 | 125 | 128 | 131 | 134 | 135 |
15 | key 16 | 18 | value 19 | 21 | example 22 |
27 | name 28 | 30 | The name of your website 31 | 33 | My awesome blog 34 |
38 | title 39 | 41 | The title of your webiste 42 | 44 | My awesome title 45 |
49 | domain 50 | 52 | The domain of your website - NOT IMPLEMENTED YET* 53 | 55 | http://es6rocks.com 56 |
60 | subtitle 61 | 63 | The subtitle of your website - NOT IMPLEMENTED YET* 64 | 66 | My awesome subtitle 67 |
71 | author 72 | 74 | Your name 75 | 77 | John da Silva 78 |
82 | keywords 83 | 85 | The keywords of the page or post - NOT IMPLEMENTED YET* 86 | 88 | JavaScript, HTML5, CSS3 89 |
93 | description 94 | 96 | Some description of your website 97 | 99 | Just a description 100 |
104 | template 105 | 107 | The template you choose to use 108 | 110 | default 111 |
115 | posts_permalink 116 | 118 | The posts permalink of your website 119 | 121 | :year/:month/:title 122 |
126 | pages_permalink 127 | 129 | The pages permalink of your website 130 | 132 | pages/:title 133 |
136 | 137 | All this information are available in any template as _**config**_. 138 | 139 | [<<< Installing Harmonic](installing.md) | [Blogging with Harmonic >>>](blogging.md) 140 | -------------------------------------------------------------------------------- /doc/img/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSRocksHQ/harmonic/c457e015632ad060eb60dbd78ad0ac5da34eae35/doc/img/config.png -------------------------------------------------------------------------------- /doc/img/harmonic-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSRocksHQ/harmonic/c457e015632ad060eb60dbd78ad0ac5da34eae35/doc/img/harmonic-cli.png -------------------------------------------------------------------------------- /doc/img/new_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSRocksHQ/harmonic/c457e015632ad060eb60dbd78ad0ac5da34eae35/doc/img/new_post.png -------------------------------------------------------------------------------- /doc/installing.md: -------------------------------------------------------------------------------- 1 | # Installing Harmonic 2 | 3 | ## Prerequirements 4 | 5 | - Node.js >= 0.10 or io.js. 6 | - npm. 7 | 8 | ## Simple installation 9 | 10 | Harmonic is available on npm: 11 | 12 | ```shell 13 | npm install harmonic -g 14 | ``` 15 | 16 | ## Bleeding edge installation 17 | 18 | You can install Harmonic directly from the GitHub repository: 19 | 20 | ```shell 21 | git clone https://github.com/es6rocks/harmonic.git 22 | cd harmonic 23 | npm link 24 | ``` 25 | 26 | If everything is ok, you can type `harmonic` in your terminal, and you will get the harmonic menu: 27 | ![Harmonic CLI](img/harmonic-cli.png) 28 | 29 | [<<< Index](README.md) | [Configuring Harmonic >>>](config.md) 30 | -------------------------------------------------------------------------------- /doc/markdown-header.md: -------------------------------------------------------------------------------- 1 | # Markdown header 2 | 3 | The markdown header is the metadata for your post or page. 4 | Let's check all available options harmonic have: 5 | 6 | 7 | 10 | 13 | 14 | 15 | 18 | 21 | 22 | 23 | 26 | 29 | 30 | 31 | 34 | 37 | 38 | 39 | 42 | 45 | 46 | 47 | 50 | 53 | 54 | 55 | 58 | 61 | 62 | 63 | 66 | 69 | 70 | 71 | 74 | 77 | 78 |
8 | layout 9 | 11 | The layout of the file (post|page) - NOT IMPLEMENTED YET* 12 |
16 | title 17 | 19 | The title of your post or page 20 |
24 | date 25 | 27 | The date in JSON format (Ex: new Date().toJSON()) 28 |
32 | comments 33 | 35 | If the page or post have comments enabled - NOT IMPLEMENTED YET* 36 |
40 | published 41 | 43 | If the page or post is available online - NOT IMPLEMENTED YET* 44 |
48 | keywords 49 | 51 | The keywords of the page or post - NOT IMPLEMENTED YET* 52 |
56 | description 57 | 59 | The post/page description 60 |
64 | categories 65 | 67 | The categories 68 |
72 | authorName 73 | 75 | The authorName 76 |
79 | 80 | NOT IMPLEMENTED YET means the default theme doesn't support those features. 81 | 82 | [<<< Blogging with Harmonic](blogging.md) | [Harmonic themes >>>](themes.md) 83 | -------------------------------------------------------------------------------- /doc/themes.md: -------------------------------------------------------------------------------- 1 | # Harmonic themes 2 | 3 | _**Introduced in Harmonic@0.1.0**_ 4 | 5 | Harmonic themes are based on the awesome [Nunjucks](https://mozilla.github.io/nunjucks/). 6 | Basically, if you want to create a Harmonic theme, you can use all the Nunjucks features. 7 | Harmonic themes are [npm packages](https://www.npmjs.com/), meaning you can easily share and use community themes. 8 | 9 | ## How to create a Harmonic theme 10 | 11 | ### npm package 12 | 13 | First, you'll need to create a `npm` package: 14 | 15 | ```bash 16 | mkdir harmonic-theme-awesome 17 | cd harmonic-theme-awesome 18 | npm init 19 | ``` 20 | 21 | Configure your npm package the way you want. 22 | In the end, you'll have a `package.json`. 23 | 24 | ### Building your theme 25 | 26 | Harmonic themes must implement 3 template files: 27 | 28 | - index.html (theme main page) 29 | - post.html (post page for a blog) 30 | - page.html (for an page) 31 | 32 | Also, you can create your own structure, like a `partials` directory with your html partials. 33 | 34 | index example: 35 | 36 | ```html 37 | 38 | 39 | 40 | 41 | {{ config.title }} 42 | 43 | 44 | 45 | {% include "partials/navigation.html" %} 46 |
47 |
48 |

49 | {{ config.title }} 50 |

51 |
52 |
53 | 54 | {% include "partials/footer.html" %} 55 | 56 | 57 | 58 | ``` 59 | 60 | ___Static files___ must be placed in a folder called `resources`. 61 | Everything inside this folder will be copied. 62 | You can also provide a `config.json` file that will be merged with the current Harmonic config. 63 | Example: 64 | 65 | ```json 66 | { 67 | "mycustomdata": "wow", 68 | "foo": "bar", 69 | "baz": ["a", "b"] 70 | } 71 | ``` 72 | 73 | Here's a sample theme structure (actually, the harmonic-theme-default uses this structure): 74 | 75 | ``` 76 | . 77 | ├── config.json 78 | ├── index.html 79 | ├── package.json 80 | ├── page.html 81 | ├── partials 82 | │   ├── footer.html 83 | │   ├── header.html 84 | │   └── navigation.html 85 | ├── post.html 86 | ├── README.md 87 | ├── resources 88 | │   ├── css 89 | │   │   └── main.css 90 | │   ├── images 91 | │   │   ├── flag-en.jpg 92 | │   │   └── flag-pt-br.jpg 93 | │   └── js 94 | │   └── main.js 95 | └── tag_archives.html 96 | ``` 97 | 98 | ## Using your theme 99 | 100 | If you're developing a new theme, you will most likely want to test it locally before publishing it. 101 | To test your theme locally, you can just install it like any other npm package, but pointing to its path: 102 | 103 | ```bash 104 | npm install ../harmonic-theme-awesome 105 | ``` 106 | 107 | Then edit your `harmonic.json` and set `"theme": "harmonic-theme-awesome"`. 108 | 109 | Note: To install the theme you must first init a new Harmonic project, or use an existing one: 110 | 111 | ```bash 112 | harmonic init "my_blog" 113 | cd my_blog 114 | npm install ../harmonic-theme-awesome 115 | ``` 116 | 117 | ## Faster development 118 | 119 | 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: 120 | 121 | ```bash 122 | # suppose you have a Harmonic theme named `harmonic-theme-awesome` in this dir: 123 | cd harmonic-theme-awesome 124 | npm link 125 | # and a Harmonic project here: 126 | cd ../my_blog 127 | npm link harmonic-theme-awesome 128 | ``` 129 | 130 | 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. 131 | 132 | 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. `;)` 133 | 134 | ## Publish 135 | 136 | 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). 137 | 138 | As Harmonic themes are just npm packages, you can publish it like any other package. 139 | Assuming you already have npm configured (registered user, etc.): 140 | 141 | ```bash 142 | npm publish ./ 143 | ``` 144 | 145 | Now, everybody can use your theme! 146 | 147 | ```bash 148 | harmonic init "my_blog" 149 | cd my_blog 150 | npm install harmonic-theme-awesome 151 | ``` 152 | 153 | [<<< Markdown Header](markdown-header.md) | [Index >>>](README.md) 154 | -------------------------------------------------------------------------------- /entry_points/README.md: -------------------------------------------------------------------------------- 1 | # Entry points 2 | 3 | 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. 4 | 5 | Put your entry point's logic in the `src/` directory. 6 | -------------------------------------------------------------------------------- /entry_points/harmonic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | require('babel-runtime/core-js').default.Promise = require('bluebird'); 5 | process.on('unhandledRejection', function(reason/*, promise*/) { 6 | console.log('Possibly Unhandled Rejection:'); 7 | console.log(reason instanceof Error ? reason.stack || reason.toString() : reason); 8 | }); 9 | 10 | module.exports = require('../dist/bin/cli/harmonic'); 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var exec = require('child_process').exec; 5 | var gulp = require('gulp'); 6 | var plugins = require('gulp-load-plugins')(); 7 | var rimraf = require('rimraf'); 8 | var mergeStream = require('merge-stream'); 9 | var globManip = require('glob-manipulate'); 10 | var build = require('./build'); 11 | var copySrc = ['**'].concat(globManip.negate(build.src.js)); 12 | 13 | // Run unit tests in complete isolation, see https://github.com/JSRocksHQ/harmonic/issues/122#issuecomment-85333442 14 | function runTests(opt, cb) { 15 | var child = exec('mocha ' + build.config.mocha + ' "' + build.distBase + 'test"', { 16 | maxBuffer: 16 * 1024 * 1024, 17 | }, function(err/*, stdout, stderr*/) { 18 | cb(opt.ignoreErrors ? null : err); 19 | }); 20 | 21 | child.stdout.pipe(process.stdout); 22 | child.stderr.pipe(process.stderr); 23 | } 24 | 25 | gulp.task('clean', function(cb) { 26 | rimraf(build.distBase, cb); 27 | }); 28 | 29 | gulp.task('build', ['clean'], function(cb) { 30 | // [[gulp4]] TODO remove srcOrderedGlobs 31 | mergeStream( 32 | plugins.srcOrderedGlobs(globManip.prefix(build.src.js, build.srcBase), { base: build.srcBase }) 33 | .pipe(plugins.eslint()) 34 | .pipe(plugins.eslint.format()) 35 | .pipe(plugins.eslint.failAfterError()) 36 | .pipe(plugins.babel(build.config.babel)) 37 | .on('error', function(err) { 38 | // Improve error logging: 39 | // workaround cmd.exe color issue, show timestamp and error type, hide call stack. 40 | plugins.util.log(err.toString()); 41 | process.exit(1); 42 | }), 43 | plugins.srcOrderedGlobs(globManip.prefix(copySrc, build.srcBase), { base: build.srcBase }) 44 | ) 45 | .pipe(gulp.dest(build.distBase)) 46 | .on('end', function() { 47 | runTests({ ignoreErrors: false }, cb); 48 | }) 49 | .resume(); 50 | }); 51 | 52 | gulp.task('default', ['clean'], function(cb) { 53 | var vinylPaths = require('vinyl-paths'); 54 | var streamify = require('stream-array'); 55 | var through2 = require('through2'); 56 | var chalk = require('chalk'); 57 | 58 | var srcToDistRelativePath = path.relative(build.srcBase, build.distBase); 59 | var SIGINTed = false; 60 | 61 | var filesFailingLint = []; 62 | var filesFailingCompile = []; 63 | function addToFailingList(list, filePath) { 64 | if (list.indexOf(filePath) === -1) list.push(filePath); 65 | } 66 | function removeFromFailingList(list, filePath) { 67 | var idx = list.indexOf(filePath); 68 | if (idx !== -1) list.splice(idx, 1); 69 | } 70 | 71 | // Diagram reference: https://github.com/JSRocksHQ/slush-es20xx/issues/5#issue-52701608 // TODO update diagram 72 | var batched = batch(function(files, cb) { 73 | files = files 74 | .pipe(plugins.plumber(function(err) { 75 | if (err.plugin === 'gulp-babel') { 76 | addToFailingList(filesFailingCompile, err.fileName); 77 | } 78 | 79 | plugins.util.log(err.toString()); 80 | })); 81 | 82 | var existingFiles = files 83 | .pipe(plugins.filter(function(file) { 84 | return file.event === 'change' || file.event === 'add'; 85 | })); 86 | 87 | mergeStream( 88 | // js pipe 89 | existingFiles 90 | .pipe(plugins.filter(build.src.js)) 91 | .pipe(plugins.eslint()) 92 | .pipe(plugins.eslint.format()) 93 | .pipe(through2.obj(function(file, enc, cb) { 94 | if (file.eslint && file.eslint.messages && file.eslint.messages.length) { 95 | addToFailingList(filesFailingLint, file.path); 96 | } else { 97 | removeFromFailingList(filesFailingLint, file.path); 98 | } 99 | cb(null, file); 100 | })) 101 | .pipe(plugins.babel(build.config.babel)) 102 | .pipe(through2.obj(function(file, enc, cb) { 103 | removeFromFailingList(filesFailingCompile, file.path); 104 | cb(null, file); 105 | })) 106 | .pipe(gulp.dest(build.distBase)), 107 | 108 | // copy pipe 109 | existingFiles 110 | .pipe(plugins.filter(copySrc)) 111 | .pipe(gulp.dest(build.distBase)), 112 | 113 | // deletion pipe 114 | files 115 | .pipe(plugins.filter(function(file) { 116 | return file.event === 'unlink'; 117 | })) 118 | .pipe(through2.obj(function(file, enc, cb) { 119 | removeFromFailingList(filesFailingLint, file.path); 120 | removeFromFailingList(filesFailingCompile, file.path); 121 | cb(null, file); 122 | })) 123 | .pipe(plugins.rename(function(filePath) { 124 | // we can't change/remove the filePath's `base`, so cd out of it in the dirname 125 | filePath.dirname = path.join(srcToDistRelativePath, filePath.dirname); 126 | })) 127 | .pipe(vinylPaths(rimraf)) 128 | ) 129 | .on('end', function() { 130 | if (filesFailingCompile.length) { 131 | var plural = filesFailingCompile.length !== 1; 132 | plugins.util.log( 133 | chalk.yellow((plural ? 'These files are' : 'This file is') + ' failing compilation:\n') 134 | + chalk.red(filesFailingCompile.join('\n')) 135 | + chalk.yellow('\nSkipping unit tests until ' + (plural ? 'these files are' : 'this file is') + ' fixed.') 136 | ); 137 | endBatch(); 138 | return; 139 | } 140 | 141 | runTests({ ignoreErrors: true }, endBatch); 142 | 143 | function endBatch() { 144 | if (filesFailingLint.length) { 145 | plugins.util.log( 146 | chalk.yellow((filesFailingLint.length !== 1 ? 'These files have' : 'This file has') + ' linting issues:\n') 147 | + chalk.red(filesFailingLint.join('\n')) 148 | ); 149 | } 150 | 151 | cb(); // must call batch's cb before checking `batched.isActive()` 152 | plugins.util.log( 153 | chalk.green('Batch completed.') 154 | + (!SIGINTed && !batched.isActive() ? ' Watching ' + chalk.magenta(build.srcBase) + ' directory for changes...' : '') 155 | ); 156 | maybeEndTask(); 157 | } 158 | }) 159 | .resume(); 160 | }); 161 | 162 | var watchStream = plugins.watch(build.srcBase + '**', { base: build.srcBase, ignoreInitial: false }, batched) 163 | .on('ready', function() { 164 | plugins.util.log('Watching ' + chalk.magenta(build.srcBase) + ' directory for changes...'); 165 | }) 166 | .on('end', maybeEndTask); 167 | 168 | var rl; 169 | if (process.platform === 'win32') { 170 | rl = require('readline').createInterface({ 171 | input: process.stdin, 172 | output: process.stdout, 173 | }).on('SIGINT', function() { 174 | process.emit('SIGINT'); 175 | }); 176 | } 177 | 178 | process.on('SIGINT', function() { 179 | if (SIGINTed) return; 180 | SIGINTed = true; 181 | watchStream.close(); 182 | }); 183 | 184 | function maybeEndTask() { 185 | if (!SIGINTed || batched.isActive()) return; 186 | if (rl) rl.close(); 187 | cb(); 188 | process.exit(0); 189 | } 190 | 191 | // Simplified fork of gulp-batch, with removed domains (async-done) and added most recent unique('path') deduping logic. 192 | // Added isActive() method which returns whether the callback is currently executing or if there are any batched/queued files waiting for execution. 193 | function batch(cb) { 194 | 195 | var batch = []; 196 | var isRunning = false; 197 | var timeout; 198 | var delay = 100; // ms 199 | 200 | function setupFlushTimeout() { 201 | if (!isRunning && batch.length) { 202 | timeout = setTimeout(flush, delay); 203 | } 204 | } 205 | 206 | function flush() { 207 | isRunning = true; 208 | cb(streamify(batch), function() { 209 | isRunning = false; 210 | setupFlushTimeout(); 211 | }); 212 | batch = []; 213 | } 214 | 215 | function doBatch(newFile) { 216 | if (!batch.some(function(file, idx) { 217 | if (newFile.path === file.path) { 218 | batch[idx] = newFile; 219 | return true; 220 | } 221 | })) batch.push(newFile); 222 | 223 | clearTimeout(timeout); 224 | setupFlushTimeout(); 225 | }; 226 | 227 | doBatch.isActive = function() { 228 | return isRunning || !!batch.length; 229 | }; 230 | 231 | return doBatch; 232 | }; 233 | }); 234 | -------------------------------------------------------------------------------- /harmonic-logo.old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JSRocksHQ/harmonic/c457e015632ad060eb60dbd78ad0ac5da34eae35/harmonic-logo.old.png -------------------------------------------------------------------------------- /harmonic-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 16 | 27 | 34 | 48 | 56 | 59 | 72 | 73 | 74 | 77 | 81 | 84 | 88 | 90 | 93 | 98 | 101 | 105 | 108 | 110 | 113 | 118 | 120 | 123 | 127 | 133 | 137 | 140 | 144 | 147 | 151 | 154 | 157 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harmonic", 3 | "description": "The next static site generator", 4 | "homepage": "https://github.com/JSRocksHQ/harmonic", 5 | "version": "0.5.1", 6 | "engines": { 7 | "node": ">=0.10" 8 | }, 9 | "preferGlobal": true, 10 | "bin": { 11 | "harmonic": "entry_points/harmonic.js" 12 | }, 13 | "files": [ 14 | "dist", 15 | "!dist/test", 16 | "entry_points", 17 | "doc" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/JSRocksHQ/harmonic.git" 22 | }, 23 | "keywords": [ 24 | "ssg", 25 | "generator", 26 | "blog", 27 | "scaffold", 28 | "markdown", 29 | "website", 30 | "static", 31 | "static generator", 32 | "static site generator", 33 | "ES6", 34 | "ES7", 35 | "ES2015", 36 | "ES2016" 37 | ], 38 | "dependencies": { 39 | "babel-runtime": "5.8.34", 40 | "bluebird": "^3.1.1", 41 | "browser-sync": "^2.11.0", 42 | "chokidar": "^1.4.2", 43 | "cli-color": "^1.1.0", 44 | "co": "^3.1.0", 45 | "co-prompt": "^1.0.0", 46 | "commander": "^2.9.0", 47 | "core-js": "^2.0.3", 48 | "dedent": "^0.6.0", 49 | "less": "^2.5.3", 50 | "marked-metadata": "0.0.6", 51 | "mkdirp": "^0.5.1", 52 | "ncp": "^2.0.0", 53 | "npm": "^3.5.3", 54 | "nunjucks": "^2.3.0", 55 | "open": "0.0.5", 56 | "permalinks": "^0.3.1", 57 | "pretty-ms": "^2.1.0", 58 | "rimraf": "^2.4.1", 59 | "stylus": "^0.53.0" 60 | }, 61 | "devDependencies": { 62 | "babel-eslint": "^5.0.0-beta6", 63 | "chalk": "^1.1.1", 64 | "glob-manipulate": "^1.1.1", 65 | "gulp": "^3.9.0", 66 | "gulp-babel": "^5.3.0", 67 | "gulp-eslint": "^1.1.1", 68 | "gulp-filter": "^3.0.1", 69 | "gulp-load-plugins": "^1.2.0", 70 | "gulp-plumber": "^1.0.1", 71 | "gulp-rename": "^1.2.2", 72 | "gulp-src-ordered-globs": "^1.0.3", 73 | "gulp-util": "^3.0.7", 74 | "gulp-watch": "^4.3.5", 75 | "merge-stream": "^1.0.0", 76 | "mocha": "^2.3.4", 77 | "should": "^8.1.1", 78 | "stream-array": "^1.1.1", 79 | "through2": "^2.0.0", 80 | "vinyl-paths": "^1.0.0" 81 | }, 82 | "author": "Jaydson Gomes (http://jaydson.org/)", 83 | "license": "MIT", 84 | "scripts": { 85 | "dev": "gulp", 86 | "test": "gulp build", 87 | "update-babel": "npm install --save --save-exact babel-runtime@5 && npm update --depth=1 babel-core", 88 | "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)", 89 | "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", 90 | "postversion": "git push --follow-tags && npm publish" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/cli/harmonic.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | 3 | import program from 'commander'; 4 | import { version } from '../config'; 5 | import { cliColor } from '../helpers'; 6 | import logo from './logo'; 7 | import { init, config, newFile, run } from './util'; 8 | import { build } from '../core'; 9 | const clc = cliColor(); 10 | 11 | program 12 | .version(version); 13 | 14 | program 15 | .command('init [path]') 16 | .description('Init your static website') 17 | .action((path = '.') => { 18 | console.log(logo); 19 | init(path); 20 | }); 21 | 22 | program 23 | .command('config [path]') 24 | .description('Config your static website') 25 | .action(async (path = '.') => { 26 | console.log(logo); 27 | await config(path); 28 | console.log(clc.info('\nharmonic.json successfully updated.')); 29 | }); 30 | 31 | program 32 | .command('build [path]') 33 | .description('Build your static website') 34 | .action((path = '.') => { 35 | build(path); 36 | }); 37 | 38 | program 39 | .command('new_post [path]') 40 | .option('--no-open', 'Don\'t open the markdown file(s) in editor') 41 | .description('Create a new post') 42 | .action((title, path = '.', { open: autoOpen }) => { 43 | newFile(path, 'post', title, autoOpen); 44 | }); 45 | 46 | program 47 | .command('new_page <title> [path]') 48 | .option('--no-open', 'Don\'t open the markdown file(s) in editor') 49 | .description('Create a new page') 50 | .action((title, path = '.', { open: autoOpen }) => { 51 | newFile(path, 'page', title, autoOpen); 52 | }); 53 | 54 | program 55 | .command('run [port] [path]') 56 | .option('--no-open', 'Don\'t open a new browser window') 57 | .description('Run your static site locally. Port is optional') 58 | .action(async (port = 9356, path = '.', { open: autoOpen }) => { 59 | await build(path); 60 | run(path, port, autoOpen); 61 | }); 62 | 63 | program.on('*', (args) => { 64 | console.error('Unknown command: ' + clc.error(args[0])); 65 | process.exit(1); 66 | }); 67 | 68 | program.parse(process.argv); 69 | 70 | // Not enough arguments 71 | if (!program.args.length) { 72 | console.log(logo); 73 | program.help(); 74 | } 75 | -------------------------------------------------------------------------------- /src/bin/cli/logo.js: -------------------------------------------------------------------------------- 1 | import { version } from '../config'; 2 | import { cliColor } from '../helpers'; 3 | 4 | const clc = cliColor(); 5 | const logo = clc.message(` 6 | |_| _ _ _ _ _ _ . _ 7 | | |(_|| | | |(_)| ||(_ 8 | ${version} 9 | `); 10 | 11 | export default logo; 12 | -------------------------------------------------------------------------------- /src/bin/cli/util.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { promisify, promisifyAll, fromNode as promiseFromNode } from 'bluebird'; 4 | import chokidar from 'chokidar'; 5 | import browserSync from 'browser-sync'; 6 | import co from 'co'; 7 | import prompt from 'co-prompt'; 8 | import mkdirp from 'mkdirp'; 9 | import { ncp } from 'ncp'; 10 | import open from 'open'; 11 | import { load as npmLoad } from 'npm'; 12 | import dd from 'dedent'; 13 | import { rootdir, postspath, pagespath } from '../config'; 14 | import { cliColor, getConfig, titleToFilename, findHarmonicRoot, displayNonInitializedFolderErrorMessage, MissingFileError } from '../helpers'; 15 | import { build } from '../core'; 16 | promisifyAll(fs); 17 | const npmLoadAsync = promisify(npmLoad); 18 | const mkdirpAsync = promisify(mkdirp); 19 | const ncpAsync = promisify(ncp); 20 | const clc = cliColor(); 21 | 22 | export { init, config, newFile, run, openFile }; 23 | 24 | // Open a file using browser, text-editor 25 | function openFile(type, sitePath, file) { 26 | if (type === 'file') { 27 | open(path.resolve(sitePath, file)); 28 | } else { 29 | // TODO unless we add a --no-watch flag, we can just delegate this to browser-sync 30 | open(file); 31 | } 32 | } 33 | 34 | function config(passedPath, _skipFindRoot = false) { 35 | const sitePath = _skipFindRoot ? passedPath : findHarmonicRoot(passedPath); 36 | 37 | if (!sitePath) { 38 | displayNonInitializedFolderErrorMessage(); 39 | throw new MissingFileError(); 40 | } 41 | 42 | const manifest = path.join(sitePath, 'harmonic.json'); 43 | 44 | return new Promise((fulfill, reject) => { 45 | co(function*() { 46 | console.log(clc.message( 47 | 'This guide will help you to create your Harmonic configuration file\n' + 48 | 'Just hit enter if you are ok with the default values.\n\n' 49 | )); 50 | 51 | /* eslint-disable camelcase */ 52 | const templateObj = { 53 | name: 'Awesome website', 54 | title: 'My awesome static website', 55 | domain: 'http://awesome.com', 56 | subtitle: 'Powered by Harmonic', 57 | author: 'Jaydson', 58 | description: 'This is the description', 59 | bio: 'Thats me', 60 | theme: 'harmonic-theme-default', 61 | preprocessor: 'stylus', 62 | posts_permalink: ':language/:year/:month/:title', 63 | pages_permalink: ':language/pages/:title', 64 | header_tokens: ['<!--', '-->'], 65 | index_posts: 10, 66 | i18n: { 67 | default: 'en', 68 | languages: ['en', 'pt-br'] 69 | } 70 | }; 71 | /* eslint-enable camelcase */ 72 | 73 | function _p(message) { 74 | return prompt(clc.message(message)); 75 | } 76 | 77 | const config = { 78 | name: (yield _p('Site name: (' + templateObj.name + ') ')) || 79 | templateObj.name, 80 | title: (yield _p('Title: (' + templateObj.title + ') ')) || 81 | templateObj.title, 82 | subtitle: (yield _p('Subtitle: (' + templateObj.subtitle + ') ')) || 83 | templateObj.subtitle, 84 | description: (yield _p('Description: (' + templateObj.description + ') ')) || 85 | templateObj.description, 86 | author: (yield _p('Author: (' + templateObj.author + ') ')) || 87 | templateObj.author, 88 | bio: (yield _p('Author bio: (' + templateObj.bio + ') ')) || 89 | templateObj.bio, 90 | theme: (yield _p('Theme: (' + templateObj.theme + ') ')) || 91 | templateObj.theme, 92 | preprocessor: (yield _p('Preprocessor: (' + templateObj.preprocessor + ') ')) || 93 | templateObj.preprocessor 94 | }; 95 | 96 | process.stdin.pause(); 97 | 98 | // create the configuration file 99 | fs.writeFile(manifest, JSON.stringify(Object.assign({}, templateObj, config), null, 4), (err) => { 100 | if (err) { 101 | reject(err); 102 | return; 103 | } 104 | fulfill(); 105 | }); 106 | })(); 107 | }); 108 | } 109 | 110 | async function init(sitePath) { 111 | const skeletonPath = path.join(rootdir, 'bin/skeleton'); 112 | 113 | await mkdirpAsync(sitePath); 114 | await ncpAsync(skeletonPath, sitePath, { stopOnErr: true }); 115 | console.log(clc.message('Harmonic skeleton started at: ' + path.resolve(sitePath))); 116 | 117 | await config(sitePath, true); 118 | 119 | console.log(clc.info('\nInstalling dependencies...')); 120 | const npm = await npmLoadAsync(); 121 | try { 122 | await promisify(npm.commands.install)(sitePath, []); 123 | } catch (e) { 124 | console.error(dd 125 | `Command ${clc.error('npm install')} failed. 126 | Make sure you are connected to the internet and execute the command above in your Harmonic skeleton directory.` 127 | ); 128 | } 129 | 130 | console.log('\n' + clc.info(dd 131 | `Your Harmonic website skeleton was successfully created! 132 | Now, browse the project directory and have fun.` 133 | )); 134 | } 135 | 136 | /** 137 | * @param {string} type - The new file's type. Can be either 'post' or 'page'. 138 | * @param {string} title - The new file's title. 139 | */ 140 | function newFile(passedPath, type, title, autoOpen) { 141 | const sitePath = findHarmonicRoot(passedPath); 142 | 143 | if (!sitePath) { 144 | displayNonInitializedFolderErrorMessage(); 145 | throw new MissingFileError(); 146 | } 147 | 148 | const langs = getConfig(sitePath).i18n.languages; 149 | // TODO use template literal + dedent 150 | const template = '<!--\n' + 151 | 'layout: ' + type + '\n' + 152 | 'title: ' + title + '\n' + 153 | 'date: ' + new Date().toJSON() + '\n' + 154 | 'comments: true\n' + 155 | 'published: true\n' + 156 | 'keywords:\n' + 157 | 'description:\n' + 158 | 'categories:\n' + 159 | '-->\n' + 160 | '# ' + title; 161 | const filedir = path.join(sitePath, type === 'post' ? postspath : pagespath); 162 | const filename = titleToFilename(title); 163 | 164 | langs.forEach((lang) => { 165 | const fileLangDir = path.join(filedir, lang); 166 | const fileW = path.join(fileLangDir, filename); 167 | mkdirp.sync(fileLangDir); 168 | fs.writeFileSync(fileW, template); 169 | if (autoOpen) { 170 | openFile('text', sitePath, fileW); 171 | } 172 | }); 173 | 174 | console.log(clc.info( 175 | type[0].toUpperCase() + type.slice(1) + 176 | ' "' + title + '" was successefuly created, check your /src/' + type + 's folder' 177 | )); 178 | } 179 | 180 | async function run(passedPath, port, autoOpen) { 181 | const sitePath = findHarmonicRoot(passedPath); 182 | 183 | if (!sitePath) { 184 | displayNonInitializedFolderErrorMessage(); 185 | throw new MissingFileError(); 186 | } 187 | 188 | let isBuilding = false; 189 | let pendingBuild = false; 190 | const bs = browserSync.create(); 191 | 192 | await promiseFromNode((cb) => { 193 | bs.init({ 194 | server: { 195 | baseDir: path.join(sitePath, 'public'), 196 | middleware: [ 197 | (req, res, next) => { 198 | if (isBuilding) { 199 | // TODO deliver browser-sync snippet when requesting a html file 200 | res.writeHead(503); 201 | res.end(); 202 | return; 203 | } 204 | next(); 205 | } 206 | ] 207 | }, 208 | port, 209 | open: autoOpen 210 | }, cb); 211 | }); 212 | 213 | console.log(clc.info(`Harmonic site is running.`)); 214 | 215 | async function buildSite() { 216 | try { 217 | await build(sitePath); 218 | if (!pendingBuild) { 219 | bs.reload(); 220 | } 221 | } catch (err) { 222 | console.error(clc.error('Build error:')); 223 | console.error(err.stack || err.toString()); 224 | } 225 | 226 | if (pendingBuild) { 227 | pendingBuild = false; 228 | await buildSite(); 229 | } 230 | } 231 | 232 | async function doBuildSite() { 233 | // TODO watcher.unwatch and watcher.add when selected theme changes in harmonic.json 234 | if (isBuilding) { 235 | pendingBuild = true; 236 | return; 237 | } 238 | 239 | isBuilding = true; 240 | await buildSite(); 241 | isBuilding = false; 242 | } 243 | 244 | const watcher = chokidar.watch(['src', 'harmonic.json', path.join('node_modules', getConfig(sitePath).theme)], { 245 | // interval: 1000, 246 | // atomic: false, 247 | ignoreInitial: true, 248 | // ignorePermissionErrors: true, 249 | cwd: sitePath 250 | }) 251 | .on('add', doBuildSite) 252 | // .on('addDir', doBuildSite) 253 | .on('change', doBuildSite) 254 | .on('unlink', doBuildSite) 255 | // .on('unlinkDir', doBuildSite) 256 | .on('error', (error) => console.error('Error occurred', error)); 257 | 258 | await promiseFromNode((cb) => watcher.on('ready', cb)); 259 | console.log(clc.info('Watching Harmonic project for changes...')); 260 | 261 | return { watcher }; 262 | } 263 | -------------------------------------------------------------------------------- /src/bin/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "blacklist": ["runtime"] 3 | } 4 | -------------------------------------------------------------------------------- /src/bin/client/harmonic-client.js: -------------------------------------------------------------------------------- 1 | /* exported Harmonic */ 2 | /* global __HARMONIC */ 3 | 4 | // Note: `__HARMONIC` is not an actual identifer, 5 | // it is the prefix of `harmonic build`'s substitution patterns. 6 | // The substitution patterns look like a property access so that 7 | // we can just whitelist `__HARMONIC` as a global identifier 8 | // instead of having to whitelist every single substitution. 9 | 10 | // TODO ESLint's `exported` directive seems to not be working correctly 11 | // with the current version. 12 | // We should probably `export` Harmonic using ES2015 module syntax and 13 | // trash the `exported` directive. 14 | class Harmonic { // eslint-disable-line no-unused-vars 15 | 16 | constructor(name) { 17 | this.name = name; 18 | } 19 | 20 | getConfig() { 21 | return __HARMONIC.CONFIG__; 22 | } 23 | 24 | getPosts() { 25 | return __HARMONIC.POSTS__; 26 | } 27 | 28 | getPages() { 29 | return __HARMONIC.PAGES__; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/bin/config.js: -------------------------------------------------------------------------------- 1 | import { normalize, join } from 'path'; 2 | 3 | // rootdir === `dist` dir 4 | export const rootdir = normalize(join(__dirname, '/../')); 5 | export { version } from '../../package.json'; 6 | export const postspath = normalize('./src/posts/'); 7 | export const pagespath = normalize('./src/pages/'); 8 | -------------------------------------------------------------------------------- /src/bin/core.js: -------------------------------------------------------------------------------- 1 | import prettyMs from 'pretty-ms'; 2 | import { findHarmonicRoot, displayNonInitializedFolderErrorMessage, MissingFileError } from './helpers'; 3 | import Harmonic from './parser'; 4 | import { cliColor } from './helpers'; 5 | const clc = cliColor(); 6 | 7 | export { build }; 8 | 9 | async function build(passedPath) { 10 | const startTime = Date.now(); 11 | 12 | const sitePath = findHarmonicRoot(passedPath); 13 | 14 | if (!sitePath) { 15 | displayNonInitializedFolderErrorMessage(); 16 | throw new MissingFileError(); 17 | } 18 | 19 | const harmonic = new Harmonic(sitePath, { quiet: false }); 20 | 21 | await harmonic.clean(); 22 | 23 | const postsDataPromise = (async () => await harmonic.generateFiles(await harmonic.getPostFiles(), 'post'))(); 24 | const pagesDataPromise = (async () => await harmonic.generateFiles(await harmonic.getPageFiles(), 'page'))(); 25 | 26 | await Promise.all([ 27 | harmonic.compileCSS(), 28 | (async () => await harmonic.generateIndex(await postsDataPromise, await pagesDataPromise))(), 29 | (async () => await harmonic.generateTagsPages(await postsDataPromise))(), 30 | (async () => await harmonic.compileJS(await postsDataPromise, await pagesDataPromise))(), 31 | (async () => await harmonic.generateRSS(await postsDataPromise, await pagesDataPromise))(), 32 | (async () => { 33 | // finish copying theme resources first to allow user resources to overwrite them. 34 | await harmonic.copyThemeResources(); 35 | await harmonic.copyUserResources(); 36 | })() 37 | ]); 38 | 39 | // TODO move logging to outside of this API? 40 | const endTime = Date.now(); 41 | console.log(clc.info(`Build completed in ${prettyMs(endTime - startTime)}.`)); 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/helpers.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join, resolve, extname, basename } from 'path'; 3 | import strIncludes from 'core-js/library/fn/string/virtual/includes'; 4 | import _cliColor from 'cli-color'; 5 | 6 | export { cliColor, isHarmonicProject, getConfig, titleToFilename, 7 | findHarmonicRoot, displayNonInitializedFolderErrorMessage, getFileName, getStructure }; 8 | 9 | // CLI color 10 | function cliColor() { 11 | return { 12 | info: _cliColor.green, 13 | error: _cliColor.red, 14 | warn: _cliColor.yellowBright, 15 | message: _cliColor.yellow 16 | }; 17 | } 18 | 19 | // Friendly message for non-initialized folder 20 | function displayNonInitializedFolderErrorMessage() { 21 | const clc = cliColor(); 22 | 23 | console.log( 24 | clc.warn('It seems this is not an Harmonic project yet. \n') + 25 | clc.warn('Check your directory or run ') + 26 | clc.info.bgWhite.italic(' harmonic init ') + 27 | clc.warn(' to start a new Harmonic project.') 28 | ); 29 | } 30 | 31 | // Check if harmonic.json file exists 32 | function isHarmonicProject(sitePath) { 33 | try { 34 | getConfig(sitePath); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Find harmonic.json. Returns path or false. 42 | function findHarmonicRoot(sitePath) { 43 | let currentPath = resolve(sitePath); 44 | let oldPath = ''; 45 | 46 | // Climb directories up until finding a `harmonic.json` file, if it is not found then return false. 47 | while (!isHarmonicProject(currentPath)) { 48 | oldPath = currentPath; 49 | currentPath = resolve(currentPath, '..'); 50 | 51 | if (oldPath === currentPath) { 52 | // reached root folder, return false; 53 | return false; 54 | } 55 | } 56 | 57 | return currentPath; 58 | } 59 | 60 | function getConfig(sitePath) { 61 | return JSON.parse(readFileSync(join(sitePath, 'harmonic.json')).toString()); 62 | } 63 | 64 | function titleToFilename(title) { 65 | return title.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase() + '.md'; 66 | } 67 | 68 | function getFileName(file) { 69 | const filename = basename(file, extname(file)); 70 | const checkDate = new Date(filename.substr(0, 10)); 71 | return isNaN(checkDate.getDate()) ? filename : filename.substr(11, filename.length); 72 | } 73 | 74 | function getStructure(defaultLang, lang, permaLink) { 75 | // If is the default language, generate in the root path 76 | if (defaultLang === lang && permaLink::strIncludes(':language')) { 77 | // TODO allow customizing the permalink format? https://github.com/JSRocksHQ/harmonic/pull/97#issuecomment-67596545 78 | return permaLink.split(':language/')[1]; 79 | } 80 | return permaLink; 81 | } 82 | 83 | // Note: class declarations are not hoisted, so this can't be listed in the exports statement at the top of this file. 84 | export class MissingFileError extends Error { 85 | constructor(file = 'harmonic.json') { 86 | super(); 87 | this.name = 'MissingFileError'; 88 | this.file = file; 89 | this.message = `Missing file: ${this.file}`; 90 | delete this.stack; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/parser.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import padStart from 'core-js/library/fn/string/virtual/pad-start'; 4 | import { promisify, promisifyAll, fromNode as promiseFromNode } from 'bluebird'; 5 | import nunjucks from 'nunjucks'; 6 | import permalinks from 'permalinks'; 7 | import MkMeta from 'marked-metadata'; 8 | import mkdirp from 'mkdirp'; 9 | import { ncp } from 'ncp'; 10 | import rimraf from 'rimraf'; 11 | import stylus from 'stylus'; 12 | import less from 'less'; 13 | import { rootdir, postspath, pagespath } from './config'; 14 | import { cliColor, getConfig, getFileName, getStructure } from './helpers'; 15 | import Theme from './theme'; 16 | promisifyAll(fs); 17 | const mkdirpAsync = promisify(mkdirp); 18 | const ncpAsync = promisify(ncp); 19 | const rimrafAsync = promisify(rimraf); 20 | 21 | const clc = cliColor(); 22 | const rMarkdownExt = /\.(?:md|markdown)$/; 23 | 24 | export default class Harmonic { 25 | /* eslint-disable camelcase */ 26 | 27 | constructor(sitePath, { quiet = true } = {}) { 28 | this.sitePath = path.resolve(sitePath); 29 | this.quiet = !!quiet; 30 | 31 | const config = Object.assign({ 32 | index_posts: 10 33 | }, getConfig(this.sitePath)); 34 | this.theme = new Theme(config.theme, this.sitePath); 35 | 36 | if (fs.existsSync(path.join(this.theme.themePath, 'config.json'))) { 37 | Object.assign(config, JSON.parse(this.theme.getFileContents('config.json'))); 38 | } 39 | 40 | this.config = config; 41 | this.nunjucksEnv = nunjucks.configure(this.theme.themePath); 42 | this.templates = {}; 43 | } 44 | 45 | async clean() { 46 | console.log(clc.warn('Cleaning up...')); 47 | await rimrafAsync(path.join(this.sitePath, 'public'), { maxBusyTries: 20 }); 48 | } 49 | 50 | async compileCSS() { 51 | const currentCSSCompiler = this.config.preprocessor; 52 | if (!currentCSSCompiler) { 53 | return; 54 | } 55 | 56 | const compiler = { 57 | 58 | less: async () => { 59 | const lessIndexPath = path.join(this.theme.themePath, 'resources/_less/index.less'); 60 | const cssDir = path.join(this.sitePath, 'public/css'); 61 | 62 | const lessInput = await fs.readFileAsync(lessIndexPath, { encoding: 'utf8' }); 63 | const { css } = await less.render(lessInput, { filename: lessIndexPath }); 64 | 65 | await mkdirpAsync(cssDir); 66 | await fs.writeFileAsync(path.join(cssDir, 'main.css'), css); 67 | 68 | console.log(clc.info('Successfully generated CSS with LESS preprocessor')); 69 | }, 70 | 71 | stylus: async () => { 72 | const stylDir = path.join(this.theme.themePath, 'resources/_stylus'); 73 | const stylIndexPath = path.join(stylDir, 'index.styl'); 74 | const cssDir = path.join(this.sitePath, 'public/css'); 75 | 76 | const stylInput = await fs.readFileAsync(stylIndexPath, { encoding: 'utf8' }); 77 | const css = await promiseFromNode((cb) => { 78 | stylus(stylInput) 79 | .set('filename', stylIndexPath) 80 | .set('paths', [path.join(stylDir, 'engine'), path.join(stylDir, 'partials')]) 81 | .render(cb); 82 | }); 83 | 84 | await mkdirpAsync(cssDir); 85 | await fs.writeFileAsync(path.join(cssDir, 'main.css'), css); 86 | 87 | console.log(clc.info('Successfully generated CSS with Stylus preprocessor')); 88 | } 89 | }; 90 | 91 | if (!compiler.hasOwnProperty(currentCSSCompiler)) { 92 | throw new Error(`Unsupported CSS preprocessor: ${currentCSSCompiler}`); 93 | } 94 | 95 | await compiler[currentCSSCompiler](); 96 | } 97 | 98 | async compileJS(postsMetadata, pagesMetadata) { 99 | const harmonicClient = (await fs.readFileAsync(`${rootdir}/bin/client/harmonic-client.js`, { encoding: 'utf8' })) 100 | .replace(/__HARMONIC\.POSTS__/g, JSON.stringify(postsMetadata)) 101 | .replace(/__HARMONIC\.PAGES__/g, JSON.stringify(pagesMetadata)) 102 | .replace(/__HARMONIC\.CONFIG__/g, JSON.stringify(this.config)); 103 | 104 | await fs.writeFileAsync(path.join(this.sitePath, 'public/harmonic.js'), harmonicClient); 105 | } 106 | 107 | async generateTagsPages(postsMetadata) { 108 | const tagTemplateNJ = nunjucks.compile(this.theme.getFileContents('index.html'), this.nunjucksEnv); 109 | const config = this.config; 110 | 111 | await Promise.all([].concat(...Object.entries(postsMetadata).map(([lang, langPosts]) => { 112 | const postsByTag = {}; 113 | langPosts.forEach((post) => { 114 | post.categories.forEach((category) => { 115 | // TODO replace with kebabCase? 116 | const tag = category.toLowerCase().trim().split(' ').join('-'); 117 | postsByTag[tag] = postsByTag[tag] || []; 118 | postsByTag[tag].push(post); 119 | }); 120 | }); 121 | 122 | return Object.entries(postsByTag).map(async ([tag, tagPosts]) => { 123 | const tagContent = tagTemplateNJ.render({ 124 | posts: tagPosts, 125 | config, 126 | category: tag, 127 | lang 128 | }); 129 | 130 | const tagPath = path.join(this.sitePath, 'public/categories', ...(config.i18n.default === lang ? [] : [lang]), tag); 131 | await mkdirpAsync(tagPath); 132 | await fs.writeFileAsync(path.join(tagPath, 'index.html'), tagContent); 133 | console.log(clc.info(`Successfully generated tag[${tag}] archive html file`)); 134 | }); 135 | }))); 136 | } 137 | 138 | async generateIndex(postsMetadata, pagesMetadata) { 139 | const indexTemplateNJ = nunjucks.compile(this.theme.getFileContents('index.html'), this.nunjucksEnv); 140 | const config = this.config; 141 | 142 | await Promise.all(Object.entries(postsMetadata).map(async ([lang, langPosts]) => { 143 | const posts = langPosts.slice(0, config.index_posts); 144 | const pages = pagesMetadata[lang] || []; 145 | 146 | const indexContent = indexTemplateNJ.render({ 147 | posts, 148 | pages, 149 | config, 150 | lang 151 | }); 152 | 153 | const indexPath = path.join(this.sitePath, 'public', ...(config.i18n.default === lang ? [] : [lang])); 154 | await mkdirpAsync(indexPath); 155 | await fs.writeFileAsync(path.join(indexPath, 'index.html'), indexContent); 156 | console.log(clc.info(`${lang}/index file successfully created`)); 157 | })); 158 | } 159 | 160 | async copyThemeResources() { 161 | await ncpAsync(path.join(this.theme.themePath, 'resources'), path.join(this.sitePath, 'public'), { stopOnErr: true }); 162 | console.log(clc.info('Theme resources copied')); 163 | } 164 | 165 | async copyUserResources() { 166 | const userResourcesPath = path.join(this.sitePath, 'resources'); 167 | await mkdirpAsync(userResourcesPath); 168 | await ncpAsync(userResourcesPath, path.join(this.sitePath, 'public'), { stopOnErr: true }); 169 | console.log(clc.info(`User resources copied`)); 170 | } 171 | 172 | getTemplate(layout) { 173 | if(!this.templates[layout]) { 174 | const templateContents = this.theme.getFileContents(`${layout}.html`); 175 | this.templates[layout] = nunjucks.compile(templateContents, this.nunjucksEnv); 176 | } 177 | return this.templates[layout]; 178 | } 179 | 180 | async generateFiles(files, fileType) { 181 | const langs = Object.keys(files); 182 | const config = this.config; 183 | const generatedFiles = {}; 184 | const currentDate = new Date(); 185 | const tokens = config.header_tokens || ['<!--', '-->']; 186 | const metadataDefaults = { 187 | layout: fileType 188 | }; 189 | 190 | const filesPath = fileType === 'post' ? postspath : pagespath; 191 | 192 | await Promise.all([].concat(...langs.map((lang) => files[lang].map(async (file) => { 193 | const md = new MkMeta(path.join(this.sitePath, filesPath, lang, file)); 194 | md.defineTokens(tokens[0], tokens[1]); 195 | 196 | const metadata = this.normalizeMetaData(md.metadata(), metadataDefaults); 197 | metadata.content = new nunjucks.runtime.SafeString(md.markdown({ 198 | crop: '<!--more-->' 199 | })); 200 | 201 | const template = this.getTemplate(metadata.layout); 202 | const filename = getFileName(file); 203 | const permalink = fileType === 'post' ? config.posts_permalink : config.pages_permalink; 204 | 205 | const filePath = permalinks({ 206 | replacements: [{ 207 | pattern: ':year', 208 | replacement: metadata.date.getFullYear() 209 | }, { 210 | pattern: ':month', 211 | replacement: (metadata.date.getMonth() + 1)::padStart(2, '0') 212 | }, { 213 | pattern: ':title', 214 | replacement: filename 215 | }, { 216 | pattern: ':language', 217 | replacement: lang 218 | }], 219 | structure: getStructure(config.i18n.default, lang, permalink) 220 | }); 221 | 222 | metadata.file = filesPath + file; 223 | metadata.filename = filename; 224 | metadata.link = filePath; 225 | metadata.lang = lang; 226 | 227 | const contentHTMLFile = template 228 | .render({ 229 | [fileType]: { 230 | content: new nunjucks.runtime.SafeString(md.markdown()), 231 | metadata 232 | }, 233 | config, 234 | lang 235 | }) 236 | .replace(/<!--[\s\S]*?-->/g, ''); 237 | 238 | if(fileType === 'page') { 239 | metadata.content = new nunjucks.runtime.SafeString(contentHTMLFile); 240 | } 241 | 242 | if (metadata.published && metadata.published === 'false') { 243 | return; 244 | } 245 | 246 | if (metadata.date && metadata.date > currentDate) { 247 | console.log(clc.info(`Skipping future ${fileType} ${metadata.filename}`)); 248 | return; 249 | } 250 | 251 | const publicFileDirPath = path.join(this.sitePath, 'public', filePath); 252 | const publicFilePath = path.join(publicFileDirPath, 'index.html'); 253 | await mkdirpAsync(publicFileDirPath); 254 | 255 | await fs.writeFileAsync(publicFilePath, contentHTMLFile); 256 | console.log(clc.info(`Successfully generated ${fileType} ${filePath}`)); 257 | 258 | generatedFiles[lang] = generatedFiles[lang] || []; 259 | generatedFiles[lang].push(metadata); 260 | })))); 261 | 262 | return fileType === 'post' ? this.sortByDate(generatedFiles) : this.sortByName(generatedFiles); 263 | } 264 | 265 | async getPostFiles() { 266 | const files = {}; 267 | 268 | await Promise.all(this.config.i18n.languages.map(async (lang) => { 269 | files[lang] = (await fs.readdirAsync(path.join(this.sitePath, postspath, lang))) 270 | .filter((p) => rMarkdownExt.test(p)); 271 | })); 272 | 273 | return files; 274 | } 275 | 276 | async getPageFiles() { 277 | const files = {}; 278 | 279 | await Promise.all(this.config.i18n.languages.map(async (lang) => { 280 | const langPath = path.join(this.sitePath, pagespath, lang); 281 | await mkdirpAsync(langPath); 282 | files[lang] = (await fs.readdirAsync(langPath)).filter((p) => rMarkdownExt.test(p)); 283 | })); 284 | 285 | return files; 286 | } 287 | 288 | async generateRSS(postsMetadata, pagesMetadata) { 289 | const rssTemplate = await fs.readFileAsync(path.join(__dirname, 'resources/rss.xml'), { encoding: 'utf8' }); 290 | const rssTemplateNJ = nunjucks.compile(rssTemplate, this.nunjucksEnv); 291 | const config = this.config; 292 | const rssAuthor = config.author_email ? `${config.author_email} ( ${config.author} )` : config.author; 293 | 294 | await Promise.all(Object.entries(postsMetadata).map(async ([lang, langPosts]) => { 295 | const posts = langPosts.slice(0, config.index_posts); 296 | const isDefaultLang = config.i18n.default === lang; 297 | const rssPath = path.join(this.sitePath, 'public', ...(isDefaultLang ? [] : [lang])); 298 | const rssLink = `${config.domain}${isDefaultLang ? '' : '/' + lang}/rss.xml`; 299 | 300 | const rssContent = rssTemplateNJ.render({ 301 | rss: { 302 | date: new Date().toUTCString(), 303 | link: rssLink, 304 | author: rssAuthor, 305 | lang: lang 306 | }, 307 | posts, 308 | config, 309 | pages: pagesMetadata 310 | }); 311 | 312 | await mkdirpAsync(rssPath); 313 | await fs.writeFileAsync(`${rssPath}/rss.xml`, rssContent); 314 | console.log(clc.info(`${lang}/rss.xml file successfully created`)); 315 | })); 316 | } 317 | 318 | sortByDate(files) { 319 | Object.values(files).forEach((filesArray) => filesArray.sort((a, b) => new Date(b.date) - new Date(a.date))); 320 | return files; 321 | } 322 | 323 | sortByName(files) { 324 | Object.values(files).forEach((filesArray) => filesArray.sort( 325 | (a, b) => a.filename.toLowerCase() > b.filename.toLowerCase() ? 1 : -1 326 | )); 327 | return files; 328 | } 329 | 330 | normalizeMetaData(data, defaults) { 331 | data.categories = (data.categories || '').split(',').map((category) => category.trim()); 332 | 333 | if (data.date) { 334 | data.date = new Date(data.date); 335 | } 336 | 337 | data.layout = data.layout || defaults.layout; 338 | 339 | return data; 340 | } 341 | 342 | } 343 | -------------------------------------------------------------------------------- /src/bin/resources/rss.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> 3 | <channel> 4 | <title>{{ config.title }} 5 | {{ config.domain }} 6 | {{ config.description }} 7 | {{ rss.author }} 8 | {{ rss.author }} 9 | {{ rss.date }} 10 | {{ rss.lang }} 11 | 12 | {% for post in posts %} 13 | 14 | {{ post.title}} 15 | {{config.domain}}/{{post.link}} 16 | 17 | {{post.date.toUTCString()}} 18 | {{config.domain}}/{{post.link}} 19 | 20 | {% endfor %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/bin/skeleton/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "harmonic-theme-default": "latest" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/bin/skeleton/resources/readme.txt: -------------------------------------------------------------------------------- 1 | Add files to this folder to have them copied to the final site during build time. 2 | 3 | PS: You can remove this file. 4 | -------------------------------------------------------------------------------- /src/bin/skeleton/src/pages/en/about.md: -------------------------------------------------------------------------------- 1 | 12 | # Hello page -------------------------------------------------------------------------------- /src/bin/skeleton/src/pages/pt-br/about.md: -------------------------------------------------------------------------------- 1 | 12 | # Olá página -------------------------------------------------------------------------------- /src/bin/skeleton/src/posts/en/hello-world.md: -------------------------------------------------------------------------------- 1 | 12 | Hello World! 13 | -------------------------------------------------------------------------------- /src/bin/skeleton/src/posts/pt-br/hello-world.md: -------------------------------------------------------------------------------- 1 | 12 | Olá mundo! 13 | -------------------------------------------------------------------------------- /src/bin/theme.js: -------------------------------------------------------------------------------- 1 | import { resolve, join } from 'path'; 2 | import { readFileSync } from 'fs'; 3 | import dd from 'dedent'; 4 | 5 | export default class Theme { 6 | 7 | constructor(name, sitePath) { 8 | if (!name) { 9 | throw new Error('Invalid theme. Please check your harmonic.json file.'); 10 | } 11 | 12 | this.name = name; 13 | this.sitePath = resolve(sitePath); 14 | this.themePath = join(this.sitePath, 'node_modules', name); 15 | } 16 | 17 | getFileContents(file) { 18 | try { 19 | return readFileSync(join(this.themePath, file), { encoding: 'utf8' }); 20 | } catch (e) { 21 | throw new Error(dd 22 | `Harmonic failed to load a theme file: "${file}". 23 | Please check your selected theme in the harmonic.json, make sure it is correctly installed and has all the necessary files.` 24 | ); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { spawn, exec } from 'child_process'; 4 | import { join } from 'path'; 5 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 6 | import { sync as rimrafSync } from 'rimraf'; 7 | import { sync as mkdirpSync } from 'mkdirp'; 8 | import 'should'; 9 | import Harmonic from '../bin/parser'; 10 | import { isHarmonicProject, getConfig, titleToFilename } from '../bin/helpers'; 11 | import { postspath, pagespath } from '../bin/config'; 12 | 13 | const testDir = join(__dirname, 'site'); 14 | const harmonicBin = join(__dirname, '../../entry_points/harmonic'); 15 | const stdoutWrite = process.stdout.write; 16 | 17 | before(() => { 18 | rimrafSync(testDir); 19 | mkdirpSync(testDir); 20 | }); 21 | 22 | after(() => { 23 | rimrafSync(testDir); 24 | }); 25 | 26 | function disableStdout() { 27 | process.stdout.write = () => {}; 28 | } 29 | function enableStdout() { 30 | process.stdout.write = stdoutWrite; 31 | } 32 | 33 | describe('CLI', () => { 34 | it('should display an error for unknown commands', (done) => { 35 | exec('node "' + harmonicBin + '" foobarbaz', (error, stdout, stderr) => { 36 | error.code.should.equal(1); 37 | stderr.should.containEql('foobarbaz'); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should init a new Harmonic site', (done) => { 43 | const harmonic = spawn('node', [harmonicBin, 'init', testDir]); 44 | harmonic.stdin.setEncoding('utf8'); 45 | harmonic.stdout.setEncoding('utf8'); 46 | 47 | harmonic.stdout.on('data', (data) => { 48 | if (data.indexOf('successfully created') === -1) { 49 | harmonic.stdin.write('\n'); 50 | return; 51 | } 52 | harmonic.stdin.end(); 53 | }); 54 | 55 | harmonic.on('close', () => { 56 | isHarmonicProject(testDir).should.be.true(); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should build the Harmonic site', (done) => { 62 | const harmonic = spawn('node', [harmonicBin, 'build', testDir]); 63 | harmonic.stdin.setEncoding('utf8'); 64 | harmonic.stdout.setEncoding('utf8'); 65 | 66 | harmonic.on('close', () => { 67 | existsSync(join(testDir, 'public')).should.be.true(); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should create and build a new post', async () => { 73 | const config = getConfig(testDir); 74 | const langs = config.i18n.languages; 75 | const title = 'new_post test'; 76 | const fileName = titleToFilename(title); 77 | const harmonic = spawn('node', [harmonicBin, 'new_post', '--no-open', title, testDir]); 78 | harmonic.stdin.setEncoding('utf8'); 79 | harmonic.stdout.setEncoding('utf8'); 80 | 81 | await new Promise((resolve) => { 82 | harmonic.on('close', () => { 83 | for (const lang of langs) { 84 | readFileSync( 85 | join(testDir, postspath, lang, fileName) 86 | ).toString().should.containEql(title); 87 | } 88 | resolve(); 89 | }); 90 | }); 91 | 92 | const harmonicBuild = spawn('node', [harmonicBin, 'build', testDir]); 93 | harmonicBuild.stdin.setEncoding('utf8'); 94 | harmonicBuild.stdout.setEncoding('utf8'); 95 | await new Promise((resolve) => { 96 | harmonicBuild.on('close', () => { 97 | const date = new Date(), 98 | year = String(date.getFullYear()), 99 | month = ('0' + (date.getMonth() + 1)).slice(-2), 100 | slug = fileName.replace(/\.md$/, ''); 101 | for (const lang of langs) { 102 | const langSegment = lang === config.i18n.default ? '.' : lang; 103 | readFileSync(join(testDir, 'public', langSegment, year, month, 104 | slug, 'index.html')).toString().should.containEql(`

${title}

`); 105 | } 106 | resolve(); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should create and build a new page', async () => { 112 | const config = getConfig(testDir); 113 | const langs = config.i18n.languages; 114 | const title = 'new_page test'; 115 | const fileName = titleToFilename(title); 116 | const harmonic = spawn('node', [harmonicBin, 'new_page', '--no-open', title, testDir]); 117 | harmonic.stdin.setEncoding('utf8'); 118 | harmonic.stdout.setEncoding('utf8'); 119 | 120 | await new Promise((resolve) => { 121 | harmonic.on('close', () => { 122 | for (const lang of langs) { 123 | readFileSync( 124 | join(testDir, pagespath, lang, fileName) 125 | ).toString().should.containEql(title); 126 | } 127 | resolve(); 128 | }); 129 | }); 130 | 131 | const harmonicBuild = spawn('node', [harmonicBin, 'build', testDir]); 132 | harmonicBuild.stdin.setEncoding('utf8'); 133 | harmonicBuild.stdout.setEncoding('utf8'); 134 | await new Promise((resolve) => { 135 | harmonicBuild.on('close', () => { 136 | const slug = fileName.replace(/\.md$/, ''); 137 | for (const lang of langs) { 138 | const langSegment = lang === config.i18n.default ? '.' : lang; 139 | readFileSync(join(testDir, 'public', langSegment, 'pages', 140 | slug, 'index.html')).toString().should.containEql(`

${title}

`); 141 | } 142 | resolve(); 143 | }); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('helpers', () => { 149 | it('.isHarmonicProject() should return whether the CWD is a Harmonic site', () => { 150 | disableStdout(); 151 | const result = isHarmonicProject(__dirname); 152 | enableStdout(); 153 | result.should.be.false(); 154 | isHarmonicProject(testDir).should.be.true(); 155 | }); 156 | 157 | it('.titleToFilename() should transform a post/page title into a filename', () => { 158 | titleToFilename('Hello World!').should.equal('hello-world.md'); 159 | }); 160 | }); 161 | 162 | describe('API', () => { 163 | it('should merge the theme\'s config into the main config', () => { 164 | const config = getConfig(testDir); 165 | const themeConfigPath = join(testDir, 'node_modules', config.theme, 'config.json'); 166 | const templateConfig = { customData: 'test' }; 167 | writeFileSync(themeConfigPath, JSON.stringify(templateConfig)); 168 | 169 | const harmonic = new Harmonic(testDir); 170 | const mergedConfig = harmonic.config; 171 | 172 | mergedConfig.should.containDeep(templateConfig); 173 | mergedConfig.should.eql(Object.assign({}, config, templateConfig)); 174 | }); 175 | }); 176 | --------------------------------------------------------------------------------