├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── config.yml ├── contents │ ├── content │ │ ├── directory-structure.md │ │ └── markdown.md │ └── getting-started │ │ ├── about.md │ │ └── install.md ├── layouts │ ├── 404.html │ ├── _default │ │ └── single.html │ ├── index.html │ └── partial │ │ ├── footer.html │ │ ├── header.html │ │ └── menu.html └── static │ ├── background1.jpg │ ├── background2.jpg │ ├── background3.jpg │ ├── css │ ├── materialize.min.css │ └── style.css │ ├── fonts │ └── roboto │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── Roboto-Thin.woff │ │ └── Roboto-Thin.woff2 │ └── js │ ├── init.js │ ├── jquery.min.js │ └── materialize.min.js ├── shard.lock ├── shard.yml ├── spec ├── fixtures │ └── configs │ │ ├── config.yml │ │ └── config_with_social.yml ├── spec_helper.cr ├── wasp │ ├── configuration_spec.cr │ └── filesystem │ │ ├── content_file_spec.cr │ │ └── front_matter_spec.cr └── wasp_spec.cr └── src ├── wasp.cr └── wasp ├── command.cr ├── commands ├── build.cr ├── config.cr ├── init.cr ├── new.cr ├── server.cr └── version.cr ├── configuration.cr ├── exception.cr ├── filesystem.cr ├── filesystem ├── content_file.cr └── front_matter.cr ├── generator.cr ├── generator ├── context.cr ├── handler.cr └── handlers │ └── contents.cr ├── server ├── handlers │ ├── livereload_handler.cr │ └── static_site_handler.cr └── static │ └── livereload.js ├── version.cr └── watcher.cr /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: crystallang/crystal 6 | environment: 7 | DOCS_PATH: "docs" 8 | GIT_USER: "icyleaf" 9 | GIT_EMAIL: "icyleaf.cn@gmail.com" 10 | GIT_REPO: "git@github.com:icyleaf/wasp.git" 11 | branches: 12 | ignore: 13 | - gh-pages 14 | working_directory: ~/wasp 15 | steps: 16 | - checkout 17 | - run: 18 | name: "Install dependencies" 19 | command: shards install 20 | - run: 21 | name: "Spec" 22 | command: crystal spec 23 | - run: 24 | name: "Build wasp" 25 | command: crystal build --release -o wasp src/wasp.cr 26 | - run: 27 | name: "Generate static site" 28 | command: ./wasp build --source $DOCS_PATH --verbose 29 | - deploy: 30 | name: "Upload to gh-page" 31 | command: | 32 | git config --global user.name "$GIT_USER" 33 | git config --global user.email "$GIT_EMAIL" 34 | cd "${DOCS_PATH}/public" 35 | git init 36 | git remote add origin $GIT_REPO 37 | git fetch origin 38 | git reset origin/gh-pages 39 | git add -A . 40 | git commit --allow-empty -m "Updating documents from master#${COMMIT_HASH}" 41 | git push origin HEAD:gh-pages 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Crystal project 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | 6 | # wasp project 7 | public/ 8 | test.cr 9 | 10 | # vs code 11 | .history 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 icyleaf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CRYSTAL_BIN ?= $(shell which crystal) 2 | PREFIX ?= /usr/local 3 | 4 | build: 5 | $(CRYSTAL_BIN) build --release -o bin/wasp src/wasp.cr $(CRFLAGS) 6 | 7 | clean: 8 | rm -rf bin 9 | mkdir bin -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wasp 2 | 3 | [![Language](https://img.shields.io/badge/language-crystal-776791.svg)](https://github.com/crystal-lang/crystal) 4 | ![Status](https://img.shields.io/badge/status-WIP-blue.svg) 5 | [![Build Status](https://img.shields.io/circleci/project/github/icyleaf/wasp/master.svg?style=flat)](https://circleci.com/gh/icyleaf/wasp) 6 | 7 | A Static Site Generator written in [Crystal](http://crystal-lang.org/) v0.26.0+. 8 | 9 | ## Document 10 | 11 | Read it [Online](https://icyleaf.github.io/wasp/) or [install](https://crystal-lang.org/docs/installation/) crystal-lang and clone the project, then to run: 12 | 13 | ``` 14 | $ make 15 | $ ./bin/wasp server -s docs --verbose 16 | Using config file: /Users/icyleaf/Development/crystal/wasp/docs 17 | Generating static files to /Users/icyleaf/Development/crystal/wasp/docs/public 18 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/getting-started/index.html 19 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/install/index.html 20 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/intro/index.html 21 | Total in 55.375 ms 22 | Watch changes in '/Users/icyleaf/Development/crystal/wasp/docs/{config.yml,contents/**/*.md,layouts/**/*.html,static/**/*}' 23 | Web Server is running at http://localhost:8624/ (bind address 127.0.0.1) 24 | Press Ctrl+C to stop 25 | ``` 26 | 27 | ## Todo 28 | 29 | This project is under development, DO NOT use it in production. 30 | 31 | - [x] site structures 32 | - [x] site configurate (default) 33 | - [x] parse markdown to html 34 | - [x] live preview with web server 35 | - [x] livereload after save content(settings/post/page) 36 | - [ ] theme template 37 | - [ ] admin panel 38 | - [ ] command line tool 39 | - [x] `config`: print site configuration 40 | - [ ] `init`: initialize a new site 41 | - [ ] `new`: create a new post 42 | - [ ] `search`: search post 43 | - [x] `build`: generate to static pages 44 | - [x] `server`: run a web server 45 | 46 | ## Inspires 47 | 48 | - [hugo](https://github.com/spf13/hugo) 49 | - [journey](https://github.com/kabukky/journey) 50 | - [dingo](https://github.com/dingoblog/dingo) 51 | 52 | ## Donate 53 | 54 | Halite is a open source, collaboratively funded project. If you run a business and are using Halite in a revenue-generating product, 55 | it would make business sense to sponsor Halite development. Individual users are also welcome to make a one time donation 56 | if Halite has helped you in your work or personal projects. 57 | 58 | You can donate via [Paypal](https://www.paypal.me/icyleaf/5). 59 | 60 | ## How to Contribute 61 | 62 | Your contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list. 63 | 64 | Here is a throughput graph of the repository for the last few weeks: 65 | 66 | All [Contributors](https://github.com/icyleaf/wasp/graphs/contributors) are on the wall. 67 | 68 | ## You may also like 69 | 70 | - [halite](https://github.com/icyleaf/halite) - HTTP Requests Client with a chainable REST API, built-in sessions and middlewares. 71 | - [totem](https://github.com/icyleaf/totem) - Load and parse a configuration file or string in JSON, YAML, dotenv formats. 72 | - [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification. 73 | - [poncho](https://github.com/icyleaf/poncho) - A .env parser/loader improved for performance. 74 | - [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another. 75 | - [fast-crystal](https://github.com/icyleaf/fast-crystal) - 💨 Writing Fast Crystal 😍 -- Collect Common Crystal idioms. 76 | 77 | ## License 78 | 79 | [MIT License](https://github.com/icyleaf/halite/blob/master/LICENSE) © icyleaf 80 | -------------------------------------------------------------------------------- /docs/config.yml: -------------------------------------------------------------------------------- 1 | title: "Wasp" 2 | subtitle: "A Static Site Generator" 3 | description: "Wasp is a Static Site Generator written in Crystal." 4 | timezone: "Asia/Shanghai" 5 | 6 | base_url: "https://icyleaf.github.io/wasp/" 7 | # theme: "nest" 8 | 9 | # avaiabled in basic/marked 10 | markdown: marked 11 | 12 | permalink: ":section/:title/" 13 | ugly_url: false 14 | -------------------------------------------------------------------------------- /docs/contents/content/directory-structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Directory structure" 3 | slug: "directory-structure.md" 4 | date: "2018-09-21T12:01:31+08:00" 5 | --- 6 | 7 | The default Wasp installation consists of a directory structure which looks like this: 8 | 9 | ``` 10 | ├── config.yml 11 | ├── contents 12 | ├── layouts 13 | │   ├── 404.html 14 | │   ├── _default 15 | │   │   └── single.html 16 | │   ├── index.html 17 | │   └── partial 18 | │   ├── footer.html 19 | │   ├── header.html 20 | │   └── menu.html 21 | └── static 22 | ├── css 23 | ├── fonts 24 | └── js 25 | ``` 26 | 27 | Here's a high level overview of each of these folders and `config.yml`. 28 | 29 | ### config.yml 30 | 31 | A mandatory configuration file of Gutenberg in YAML format. It is explained in details in the [Configuration page](/configuration). 32 | 33 | ### contents 34 | 35 | Where all your markup content lies: this will be mostly comprised of .md files. 36 | Each folder in the content directory represents a section that contains pages : your .md files. 37 | 38 | ### layouts 39 | 40 | Contains all the Crinja templates that will be used to render this site. 41 | Have a look at the Templates to learn more about default templates and available variables. 42 | 43 | ### static 44 | 45 | Contains any kind of files. All the files/folders in the static folder will be copied as-is in the output directory. 46 | -------------------------------------------------------------------------------- /docs/contents/content/markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Intro Markdown" 3 | slug: "intro-markdown" 4 | date: "2017-05-03T15:00:31+08:00" 5 | --- 6 | 7 | Wasp use [Markd](httpshttps://github.com/icyleaf/markd) Markdown library. it is 100% [CommonMark](http://commonmark.org/) support, But [GFM](https://github.github.com/gfm/) is still in development. 8 | 9 | 10 | # Markdown: Syntax 11 | 12 | * [Overview](#overview) 13 | * [Philosophy](#philosophy) 14 | * [Inline HTML](#html) 15 | * [Automatic Escaping for Special Characters](#autoescape) 16 | * [Block Elements](#block) 17 | * [Paragraphs and Line Breaks](#p) 18 | * [Headers](#header) 19 | * [Blockquotes](#blockquote) 20 | * [Lists](#list) 21 | * [Code Blocks](#precode) 22 | * [Horizontal Rules](#hr) 23 | * [Span Elements](#span) 24 | * [Links](#link) 25 | * [Emphasis](#em) 26 | * [Code](#code) 27 | * [Images](#img) 28 | * [Miscellaneous](#misc) 29 | * [Backslash Escapes](#backslash) 30 | * [Automatic Links](#autolink) 31 | 32 | 33 | **Note:** This document is itself written using Markdown; you 34 | can [see the source for it by adding '.text' to the URL][src]. 35 | 36 | [src]: /projects/markdown/syntax.text 37 | 38 | * * * 39 | 40 |

Overview

41 | 42 |

Philosophy

43 | 44 | Markdown is intended to be as easy-to-read and easy-to-write as is feasible. 45 | 46 | Readability, however, is emphasized above all else. A Markdown-formatted 47 | document should be publishable as-is, as plain text, without looking 48 | like it's been marked up with tags or formatting instructions. While 49 | Markdown's syntax has been influenced by several existing text-to-HTML 50 | filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], 51 | [Grutatext] [5], and [EtText] [6] -- the single biggest source of 52 | inspiration for Markdown's syntax is the format of plain text email. 53 | 54 | [1]: http://docutils.sourceforge.net/mirror/setext.html 55 | [2]: http://www.aaronsw.com/2002/atx/ 56 | [3]: http://textism.com/tools/textile/ 57 | [4]: http://docutils.sourceforge.net/rst.html 58 | [5]: http://www.triptico.com/software/grutatxt.html 59 | [6]: http://ettext.taint.org/doc/ 60 | 61 | To this end, Markdown's syntax is comprised entirely of punctuation 62 | characters, which punctuation characters have been carefully chosen so 63 | as to look like what they mean. E.g., asterisks around a word actually 64 | look like \*emphasis\*. Markdown lists look like, well, lists. Even 65 | blockquotes look like quoted passages of text, assuming you've ever 66 | used email. 67 | 68 | 69 | 70 |

Inline HTML

71 | 72 | Markdown's syntax is intended for one purpose: to be used as a 73 | format for *writing* for the web. 74 | 75 | Markdown is not a replacement for HTML, or even close to it. Its 76 | syntax is very small, corresponding only to a very small subset of 77 | HTML tags. The idea is *not* to create a syntax that makes it easier 78 | to insert HTML tags. In my opinion, HTML tags are already easy to 79 | insert. The idea for Markdown is to make it easy to read, write, and 80 | edit prose. HTML is a *publishing* format; Markdown is a *writing* 81 | format. Thus, Markdown's formatting syntax only addresses issues that 82 | can be conveyed in plain text. 83 | 84 | For any markup that is not covered by Markdown's syntax, you simply 85 | use HTML itself. There's no need to preface it or delimit it to 86 | indicate that you're switching from Markdown to HTML; you just use 87 | the tags. 88 | 89 | The only restrictions are that block-level HTML elements -- e.g. `
`, 90 | ``, `
`, `

`, etc. -- must be separated from surrounding 91 | content by blank lines, and the start and end tags of the block should 92 | not be indented with tabs or spaces. Markdown is smart enough not 93 | to add extra (unwanted) `

` tags around HTML block-level tags. 94 | 95 | For example, to add an HTML table to a Markdown article: 96 | 97 | This is a regular paragraph. 98 | 99 |

100 | 101 | 102 | 103 |
Foo
104 | 105 | This is another regular paragraph. 106 | 107 | Note that Markdown formatting syntax is not processed within block-level 108 | HTML tags. E.g., you can't use Markdown-style `*emphasis*` inside an 109 | HTML block. 110 | 111 | Span-level HTML tags -- e.g. ``, ``, or `` -- can be 112 | used anywhere in a Markdown paragraph, list item, or header. If you 113 | want, you can even use HTML tags instead of Markdown formatting; e.g. if 114 | you'd prefer to use HTML `` or `` tags instead of Markdown's 115 | link or image syntax, go right ahead. 116 | 117 | Unlike block-level HTML tags, Markdown syntax *is* processed within 118 | span-level tags. 119 | 120 | 121 |

Automatic Escaping for Special Characters

122 | 123 | In HTML, there are two characters that demand special treatment: `<` 124 | and `&`. Left angle brackets are used to start tags; ampersands are 125 | used to denote HTML entities. If you want to use them as literal 126 | characters, you must escape them as entities, e.g. `<`, and 127 | `&`. 128 | 129 | Ampersands in particular are bedeviling for web writers. If you want to 130 | write about 'AT&T', you need to write '`AT&T`'. You even need to 131 | escape ampersands within URLs. Thus, if you want to link to: 132 | 133 | http://images.google.com/images?num=30&q=larry+bird 134 | 135 | you need to encode the URL as: 136 | 137 | http://images.google.com/images?num=30&q=larry+bird 138 | 139 | in your anchor tag `href` attribute. Needless to say, this is easy to 140 | forget, and is probably the single most common source of HTML validation 141 | errors in otherwise well-marked-up web sites. 142 | 143 | Markdown allows you to use these characters naturally, taking care of 144 | all the necessary escaping for you. If you use an ampersand as part of 145 | an HTML entity, it remains unchanged; otherwise it will be translated 146 | into `&`. 147 | 148 | So, if you want to include a copyright symbol in your article, you can write: 149 | 150 | © 151 | 152 | and Markdown will leave it alone. But if you write: 153 | 154 | AT&T 155 | 156 | Markdown will translate it to: 157 | 158 | AT&T 159 | 160 | Similarly, because Markdown supports [inline HTML](#html), if you use 161 | angle brackets as delimiters for HTML tags, Markdown will treat them as 162 | such. But if you write: 163 | 164 | 4 < 5 165 | 166 | Markdown will translate it to: 167 | 168 | 4 < 5 169 | 170 | However, inside Markdown code spans and blocks, angle brackets and 171 | ampersands are *always* encoded automatically. This makes it easy to use 172 | Markdown to write about HTML code. (As opposed to raw HTML, which is a 173 | terrible format for writing about HTML syntax, because every single `<` 174 | and `&` in your example code needs to be escaped.) 175 | 176 | 177 | * * * 178 | 179 | 180 |

Block Elements

181 | 182 | 183 |

Paragraphs and Line Breaks

184 | 185 | A paragraph is simply one or more consecutive lines of text, separated 186 | by one or more blank lines. (A blank line is any line that looks like a 187 | blank line -- a line containing nothing but spaces or tabs is considered 188 | blank.) Normal paragraphs should not be indented with spaces or tabs. 189 | 190 | The implication of the "one or more consecutive lines of text" rule is 191 | that Markdown supports "hard-wrapped" text paragraphs. This differs 192 | significantly from most other text-to-HTML formatters (including Movable 193 | Type's "Convert Line Breaks" option) which translate every line break 194 | character in a paragraph into a `
` tag. 195 | 196 | When you *do* want to insert a `
` break tag using Markdown, you 197 | end a line with two or more spaces, then type return. 198 | 199 | Yes, this takes a tad more effort to create a `
`, but a simplistic 200 | "every line break is a `
`" rule wouldn't work for Markdown. 201 | Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l] 202 | work best -- and look better -- when you format them with hard breaks. 203 | 204 | [bq]: #blockquote 205 | [l]: #list 206 | 207 | 208 | 209 | 210 | 211 | Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. 212 | 213 | Setext-style headers are "underlined" using equal signs (for first-level 214 | headers) and dashes (for second-level headers). For example: 215 | 216 | This is an H1 217 | ============= 218 | 219 | This is an H2 220 | ------------- 221 | 222 | Any number of underlining `=`'s or `-`'s will work. 223 | 224 | Atx-style headers use 1-6 hash characters at the start of the line, 225 | corresponding to header levels 1-6. For example: 226 | 227 | # This is an H1 228 | 229 | ## This is an H2 230 | 231 | ###### This is an H6 232 | 233 | Optionally, you may "close" atx-style headers. This is purely 234 | cosmetic -- you can use this if you think it looks better. The 235 | closing hashes don't even need to match the number of hashes 236 | used to open the header. (The number of opening hashes 237 | determines the header level.) : 238 | 239 | # This is an H1 # 240 | 241 | ## This is an H2 ## 242 | 243 | ### This is an H3 ###### 244 | 245 | 246 |

Blockquotes

247 | 248 | Markdown uses email-style `>` characters for blockquoting. If you're 249 | familiar with quoting passages of text in an email message, then you 250 | know how to create a blockquote in Markdown. It looks best if you hard 251 | wrap the text and put a `>` before every line: 252 | 253 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 254 | > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 255 | > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 256 | > 257 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 258 | > id sem consectetuer libero luctus adipiscing. 259 | 260 | Markdown allows you to be lazy and only put the `>` before the first 261 | line of a hard-wrapped paragraph: 262 | 263 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 264 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 265 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 266 | 267 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 268 | id sem consectetuer libero luctus adipiscing. 269 | 270 | Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by 271 | adding additional levels of `>`: 272 | 273 | > This is the first level of quoting. 274 | > 275 | > > This is nested blockquote. 276 | > 277 | > Back to the first level. 278 | 279 | Blockquotes can contain other Markdown elements, including headers, lists, 280 | and code blocks: 281 | 282 | > ## This is a header. 283 | > 284 | > 1. This is the first list item. 285 | > 2. This is the second list item. 286 | > 287 | > Here's some example code: 288 | > 289 | > return shell_exec("echo $input | $markdown_script"); 290 | 291 | Any decent text editor should make email-style quoting easy. For 292 | example, with BBEdit, you can make a selection and choose Increase 293 | Quote Level from the Text menu. 294 | 295 | 296 |

Lists

297 | 298 | Markdown supports ordered (numbered) and unordered (bulleted) lists. 299 | 300 | Unordered lists use asterisks, pluses, and hyphens -- interchangably 301 | -- as list markers: 302 | 303 | * Red 304 | * Green 305 | * Blue 306 | 307 | is equivalent to: 308 | 309 | + Red 310 | + Green 311 | + Blue 312 | 313 | and: 314 | 315 | - Red 316 | - Green 317 | - Blue 318 | 319 | Ordered lists use numbers followed by periods: 320 | 321 | 1. Bird 322 | 2. McHale 323 | 3. Parish 324 | 325 | It's important to note that the actual numbers you use to mark the 326 | list have no effect on the HTML output Markdown produces. The HTML 327 | Markdown produces from the above list is: 328 | 329 |
    330 |
  1. Bird
  2. 331 |
  3. McHale
  4. 332 |
  5. Parish
  6. 333 |
334 | 335 | If you instead wrote the list in Markdown like this: 336 | 337 | 1. Bird 338 | 1. McHale 339 | 1. Parish 340 | 341 | or even: 342 | 343 | 3. Bird 344 | 1. McHale 345 | 8. Parish 346 | 347 | you'd get the exact same HTML output. The point is, if you want to, 348 | you can use ordinal numbers in your ordered Markdown lists, so that 349 | the numbers in your source match the numbers in your published HTML. 350 | But if you want to be lazy, you don't have to. 351 | 352 | If you do use lazy list numbering, however, you should still start the 353 | list with the number 1. At some point in the future, Markdown may support 354 | starting ordered lists at an arbitrary number. 355 | 356 | List markers typically start at the left margin, but may be indented by 357 | up to three spaces. List markers must be followed by one or more spaces 358 | or a tab. 359 | 360 | To make lists look nice, you can wrap items with hanging indents: 361 | 362 | * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 363 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, 364 | viverra nec, fringilla in, laoreet vitae, risus. 365 | * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. 366 | Suspendisse id sem consectetuer libero luctus adipiscing. 367 | 368 | But if you want to be lazy, you don't have to: 369 | 370 | * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 371 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, 372 | viverra nec, fringilla in, laoreet vitae, risus. 373 | * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. 374 | Suspendisse id sem consectetuer libero luctus adipiscing. 375 | 376 | If list items are separated by blank lines, Markdown will wrap the 377 | items in `

` tags in the HTML output. For example, this input: 378 | 379 | * Bird 380 | * Magic 381 | 382 | will turn into: 383 | 384 |

    385 |
  • Bird
  • 386 |
  • Magic
  • 387 |
388 | 389 | But this: 390 | 391 | * Bird 392 | 393 | * Magic 394 | 395 | will turn into: 396 | 397 |
    398 |
  • Bird

  • 399 |
  • Magic

  • 400 |
401 | 402 | List items may consist of multiple paragraphs. Each subsequent 403 | paragraph in a list item must be indented by either 4 spaces 404 | or one tab: 405 | 406 | 1. This is a list item with two paragraphs. Lorem ipsum dolor 407 | sit amet, consectetuer adipiscing elit. Aliquam hendrerit 408 | mi posuere lectus. 409 | 410 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet 411 | vitae, risus. Donec sit amet nisl. Aliquam semper ipsum 412 | sit amet velit. 413 | 414 | 2. Suspendisse id sem consectetuer libero luctus adipiscing. 415 | 416 | It looks nice if you indent every line of the subsequent 417 | paragraphs, but here again, Markdown will allow you to be 418 | lazy: 419 | 420 | * This is a list item with two paragraphs. 421 | 422 | This is the second paragraph in the list item. You're 423 | only required to indent the first line. Lorem ipsum dolor 424 | sit amet, consectetuer adipiscing elit. 425 | 426 | * Another item in the same list. 427 | 428 | To put a blockquote within a list item, the blockquote's `>` 429 | delimiters need to be indented: 430 | 431 | * A list item with a blockquote: 432 | 433 | > This is a blockquote 434 | > inside a list item. 435 | 436 | To put a code block within a list item, the code block needs 437 | to be indented *twice* -- 8 spaces or two tabs: 438 | 439 | * A list item with a code block: 440 | 441 | 442 | 443 | 444 | It's worth noting that it's possible to trigger an ordered list by 445 | accident, by writing something like this: 446 | 447 | 1986. What a great season. 448 | 449 | In other words, a *number-period-space* sequence at the beginning of a 450 | line. To avoid this, you can backslash-escape the period: 451 | 452 | 1986\. What a great season. 453 | 454 | 455 | 456 |

Code Blocks

457 | 458 | Pre-formatted code blocks are used for writing about programming or 459 | markup source code. Rather than forming normal paragraphs, the lines 460 | of a code block are interpreted literally. Markdown wraps a code block 461 | in both `
` and `` tags.
462 | 
463 | To produce a code block in Markdown, simply indent every line of the
464 | block by at least 4 spaces or 1 tab. For example, given this input:
465 | 
466 |     This is a normal paragraph:
467 | 
468 |         This is a code block.
469 | 
470 | Markdown will generate:
471 | 
472 |     

This is a normal paragraph:

473 | 474 |
This is a code block.
475 |     
476 | 477 | One level of indentation -- 4 spaces or 1 tab -- is removed from each 478 | line of the code block. For example, this: 479 | 480 | Here is an example of AppleScript: 481 | 482 | tell application "Foo" 483 | beep 484 | end tell 485 | 486 | will turn into: 487 | 488 |

Here is an example of AppleScript:

489 | 490 |
tell application "Foo"
491 |         beep
492 |     end tell
493 |     
494 | 495 | A code block continues until it reaches a line that is not indented 496 | (or the end of the article). 497 | 498 | Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) 499 | are automatically converted into HTML entities. This makes it very 500 | easy to include example HTML source code using Markdown -- just paste 501 | it and indent it, and Markdown will handle the hassle of encoding the 502 | ampersands and angle brackets. For example, this: 503 | 504 | 507 | 508 | will turn into: 509 | 510 |
<div class="footer">
511 |         &copy; 2004 Foo Corporation
512 |     </div>
513 |     
514 | 515 | Regular Markdown syntax is not processed within code blocks. E.g., 516 | asterisks are just literal asterisks within a code block. This means 517 | it's also easy to use Markdown to write about Markdown's own syntax. 518 | 519 | 520 | 521 |

Horizontal Rules

522 | 523 | You can produce a horizontal rule tag (`
`) by placing three or 524 | more hyphens, asterisks, or underscores on a line by themselves. If you 525 | wish, you may use spaces between the hyphens or asterisks. Each of the 526 | following lines will produce a horizontal rule: 527 | 528 | * * * 529 | 530 | *** 531 | 532 | ***** 533 | 534 | - - - 535 | 536 | --------------------------------------- 537 | 538 | 539 | * * * 540 | 541 |

Span Elements

542 | 543 | 544 | 545 | Markdown supports two style of links: *inline* and *reference*. 546 | 547 | In both styles, the link text is delimited by [square brackets]. 548 | 549 | To create an inline link, use a set of regular parentheses immediately 550 | after the link text's closing square bracket. Inside the parentheses, 551 | put the URL where you want the link to point, along with an *optional* 552 | title for the link, surrounded in quotes. For example: 553 | 554 | This is [an example](http://example.com/ "Title") inline link. 555 | 556 | [This link](http://example.net/) has no title attribute. 557 | 558 | Will produce: 559 | 560 |

This is 561 | an example inline link.

562 | 563 |

This link has no 564 | title attribute.

565 | 566 | If you're referring to a local resource on the same server, you can 567 | use relative paths: 568 | 569 | See my [About](/about/) page for details. 570 | 571 | Reference-style links use a second set of square brackets, inside 572 | which you place a label of your choosing to identify the link: 573 | 574 | This is [an example][id] reference-style link. 575 | 576 | You can optionally use a space to separate the sets of brackets: 577 | 578 | This is [an example] [id] reference-style link. 579 | 580 | Then, anywhere in the document, you define your link label like this, 581 | on a line by itself: 582 | 583 | [id]: http://example.com/ "Optional Title Here" 584 | 585 | That is: 586 | 587 | * Square brackets containing the link identifier (optionally 588 | indented from the left margin using up to three spaces); 589 | * followed by a colon; 590 | * followed by one or more spaces (or tabs); 591 | * followed by the URL for the link; 592 | * optionally followed by a title attribute for the link, enclosed 593 | in double or single quotes, or enclosed in parentheses. 594 | 595 | The following three link definitions are equivalent: 596 | 597 | [foo]: http://example.com/ "Optional Title Here" 598 | [foo]: http://example.com/ 'Optional Title Here' 599 | [foo]: http://example.com/ (Optional Title Here) 600 | 601 | **Note:** There is a known bug in Markdown.pl 1.0.1 which prevents 602 | single quotes from being used to delimit link titles. 603 | 604 | The link URL may, optionally, be surrounded by angle brackets: 605 | 606 | [id]: "Optional Title Here" 607 | 608 | You can put the title attribute on the next line and use extra spaces 609 | or tabs for padding, which tends to look better with longer URLs: 610 | 611 | [id]: http://example.com/longish/path/to/resource/here 612 | "Optional Title Here" 613 | 614 | Link definitions are only used for creating links during Markdown 615 | processing, and are stripped from your document in the HTML output. 616 | 617 | Link definition names may consist of letters, numbers, spaces, and 618 | punctuation -- but they are *not* case sensitive. E.g. these two 619 | links: 620 | 621 | [link text][a] 622 | [link text][A] 623 | 624 | are equivalent. 625 | 626 | The *implicit link name* shortcut allows you to omit the name of the 627 | link, in which case the link text itself is used as the name. 628 | Just use an empty set of square brackets -- e.g., to link the word 629 | "Google" to the google.com web site, you could simply write: 630 | 631 | [Google][] 632 | 633 | And then define the link: 634 | 635 | [Google]: http://google.com/ 636 | 637 | Because link names may contain spaces, this shortcut even works for 638 | multiple words in the link text: 639 | 640 | Visit [Daring Fireball][] for more information. 641 | 642 | And then define the link: 643 | 644 | [Daring Fireball]: http://daringfireball.net/ 645 | 646 | Link definitions can be placed anywhere in your Markdown document. I 647 | tend to put them immediately after each paragraph in which they're 648 | used, but if you want, you can put them all at the end of your 649 | document, sort of like footnotes. 650 | 651 | Here's an example of reference links in action: 652 | 653 | I get 10 times more traffic from [Google] [1] than from 654 | [Yahoo] [2] or [MSN] [3]. 655 | 656 | [1]: http://google.com/ "Google" 657 | [2]: http://search.yahoo.com/ "Yahoo Search" 658 | [3]: http://search.msn.com/ "MSN Search" 659 | 660 | Using the implicit link name shortcut, you could instead write: 661 | 662 | I get 10 times more traffic from [Google][] than from 663 | [Yahoo][] or [MSN][]. 664 | 665 | [google]: http://google.com/ "Google" 666 | [yahoo]: http://search.yahoo.com/ "Yahoo Search" 667 | [msn]: http://search.msn.com/ "MSN Search" 668 | 669 | Both of the above examples will produce the following HTML output: 670 | 671 |

I get 10 times more traffic from Google than from 673 | Yahoo 674 | or MSN.

675 | 676 | For comparison, here is the same paragraph written using 677 | Markdown's inline link style: 678 | 679 | I get 10 times more traffic from [Google](http://google.com/ "Google") 680 | than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or 681 | [MSN](http://search.msn.com/ "MSN Search"). 682 | 683 | The point of reference-style links is not that they're easier to 684 | write. The point is that with reference-style links, your document 685 | source is vastly more readable. Compare the above examples: using 686 | reference-style links, the paragraph itself is only 81 characters 687 | long; with inline-style links, it's 176 characters; and as raw HTML, 688 | it's 234 characters. In the raw HTML, there's more markup than there 689 | is text. 690 | 691 | With Markdown's reference-style links, a source document much more 692 | closely resembles the final output, as rendered in a browser. By 693 | allowing you to move the markup-related metadata out of the paragraph, 694 | you can add links without interrupting the narrative flow of your 695 | prose. 696 | 697 | 698 |

Emphasis

699 | 700 | Markdown treats asterisks (`*`) and underscores (`_`) as indicators of 701 | emphasis. Text wrapped with one `*` or `_` will be wrapped with an 702 | HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML 703 | `` tag. E.g., this input: 704 | 705 | *single asterisks* 706 | 707 | _single underscores_ 708 | 709 | **double asterisks** 710 | 711 | __double underscores__ 712 | 713 | will produce: 714 | 715 | single asterisks 716 | 717 | single underscores 718 | 719 | double asterisks 720 | 721 | double underscores 722 | 723 | You can use whichever style you prefer; the lone restriction is that 724 | the same character must be used to open and close an emphasis span. 725 | 726 | Emphasis can be used in the middle of a word: 727 | 728 | un*frigging*believable 729 | 730 | But if you surround an `*` or `_` with spaces, it'll be treated as a 731 | literal asterisk or underscore. 732 | 733 | To produce a literal asterisk or underscore at a position where it 734 | would otherwise be used as an emphasis delimiter, you can backslash 735 | escape it: 736 | 737 | \*this text is surrounded by literal asterisks\* 738 | 739 | 740 | 741 |

Code

742 | 743 | To indicate a span of code, wrap it with backtick quotes (`` ` ``). 744 | Unlike a pre-formatted code block, a code span indicates code within a 745 | normal paragraph. For example: 746 | 747 | Use the `printf()` function. 748 | 749 | will produce: 750 | 751 |

Use the printf() function.

752 | 753 | To include a literal backtick character within a code span, you can use 754 | multiple backticks as the opening and closing delimiters: 755 | 756 | ``There is a literal backtick (`) here.`` 757 | 758 | which will produce this: 759 | 760 |

There is a literal backtick (`) here.

761 | 762 | The backtick delimiters surrounding a code span may include spaces -- 763 | one after the opening, one before the closing. This allows you to place 764 | literal backtick characters at the beginning or end of a code span: 765 | 766 | A single backtick in a code span: `` ` `` 767 | 768 | A backtick-delimited string in a code span: `` `foo` `` 769 | 770 | will produce: 771 | 772 |

A single backtick in a code span: `

773 | 774 |

A backtick-delimited string in a code span: `foo`

775 | 776 | With a code span, ampersands and angle brackets are encoded as HTML 777 | entities automatically, which makes it easy to include example HTML 778 | tags. Markdown will turn this: 779 | 780 | Please don't use any `` tags. 781 | 782 | into: 783 | 784 |

Please don't use any <blink> tags.

785 | 786 | You can write this: 787 | 788 | `—` is the decimal-encoded equivalent of `—`. 789 | 790 | to produce: 791 | 792 |

&#8212; is the decimal-encoded 793 | equivalent of &mdash;.

794 | 795 | 796 | 797 |

Images

798 | 799 | Admittedly, it's fairly difficult to devise a "natural" syntax for 800 | placing images into a plain text document format. 801 | 802 | Markdown uses an image syntax that is intended to resemble the syntax 803 | for links, allowing for two styles: *inline* and *reference*. 804 | 805 | Inline image syntax looks like this: 806 | 807 | ![Alt text](/path/to/img.jpg) 808 | 809 | ![Alt text](/path/to/img.jpg "Optional title") 810 | 811 | That is: 812 | 813 | * An exclamation mark: `!`; 814 | * followed by a set of square brackets, containing the `alt` 815 | attribute text for the image; 816 | * followed by a set of parentheses, containing the URL or path to 817 | the image, and an optional `title` attribute enclosed in double 818 | or single quotes. 819 | 820 | Reference-style image syntax looks like this: 821 | 822 | ![Alt text][id] 823 | 824 | Where "id" is the name of a defined image reference. Image references 825 | are defined using syntax identical to link references: 826 | 827 | [id]: url/to/image "Optional title attribute" 828 | 829 | As of this writing, Markdown has no syntax for specifying the 830 | dimensions of an image; if this is important to you, you can simply 831 | use regular HTML `` tags. 832 | 833 | 834 | * * * 835 | 836 | 837 |

Miscellaneous

838 | 839 | 840 | 841 | Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this: 842 | 843 | 844 | 845 | Markdown will turn this into: 846 | 847 | http://example.com/ 848 | 849 | Automatic links for email addresses work similarly, except that 850 | Markdown will also perform a bit of randomized decimal and hex 851 | entity-encoding to help obscure your address from address-harvesting 852 | spambots. For example, Markdown will turn this: 853 | 854 | 855 | 856 | into something like this: 857 | 858 | address@exa 861 | mple.com 862 | 863 | which will render in a browser as a clickable link to "address@example.com". 864 | 865 | (This sort of entity-encoding trick will indeed fool many, if not 866 | most, address-harvesting bots, but it definitely won't fool all of 867 | them. It's better than nothing, but an address published in this way 868 | will probably eventually start receiving spam.) 869 | 870 | 871 | 872 |

Backslash Escapes

873 | 874 | Markdown allows you to use backslash escapes to generate literal 875 | characters which would otherwise have special meaning in Markdown's 876 | formatting syntax. For example, if you wanted to surround a word 877 | with literal asterisks (instead of an HTML `` tag), you can use 878 | backslashes before the asterisks, like this: 879 | 880 | \*literal asterisks\* 881 | 882 | Markdown provides backslash escapes for the following characters: 883 | 884 | \ backslash 885 | ` backtick 886 | * asterisk 887 | _ underscore 888 | {} curly braces 889 | [] square brackets 890 | () parentheses 891 | # hash mark 892 | + plus sign 893 | - minus sign (hyphen) 894 | . dot 895 | ! exclamation mark 896 | -------------------------------------------------------------------------------- /docs/contents/getting-started/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About" 3 | slug: "about" 4 | date: "2017-06-02T15:00:31+08:00" 5 | --- 6 | 7 | # What is Wasp? 8 | 9 | ![Status](https://img.shields.io/badge/status-WIP-blue.svg) 10 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/icyleaf/wasp/blob/master/LICENSE) 11 | [![Build Status](https://img.shields.io/circleci/project/github/icyleaf/wasp/master.svg?style=flat)](https://circleci.com/gh/icyleaf/wasp) 12 | 13 | A Static Site Generator written in [Crystal](http://crystal-lang.org/). 14 | 15 | ## Documents 16 | 17 | Read it [Online](https://icyleaf.github.io/wasp/) or [install](https://crystal-lang.org/docs/installation/) crystal-lang and clone the project, then to run: 18 | 19 | ``` 20 | $ make 21 | $ ./bin/wasp server -s docs --verbose 22 | Using config file: /Users/icyleaf/Development/crystal/wasp/docs 23 | Generating static files to /Users/icyleaf/Development/crystal/wasp/docs/public 24 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/getting-started/index.html 25 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/install/index.html 26 | Write to /Users/icyleaf/Development/crystal/wasp/docs/public/guide/intro/index.html 27 | Total in 55.375 ms 28 | Watch changes in '/Users/icyleaf/Development/crystal/wasp/docs/{config.yml,contents/**/*.md,layouts/**/*.html,static/**/*}' 29 | Web Server is running at http://localhost:8624/ (bind address 127.0.0.1) 30 | Press Ctrl+C to stop 31 | ``` 32 | 33 | ## Inspires 34 | 35 | - [hugo](https://github.com/spf13/hugo) 36 | - [journey](https://github.com/kabukky/journey) 37 | - [dingo](https://github.com/dingoblog/dingo) 38 | 39 | ## Contributing 40 | 41 | 1. [Fork me](https://github.com/icyleaf/wasp/fork) 42 | 2. Create your feature branch (git checkout -b my-new-feature) 43 | 3. Commit your changes (git commit -am 'Add some feature') 44 | 4. Push to the branch (git push origin my-new-feature) 45 | 5. Create a new Pull Request 46 | 47 | ## Contributors 48 | 49 | - [icyleaf](https://github.com/icyleaf) - creator, maintainer 50 | -------------------------------------------------------------------------------- /docs/contents/getting-started/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installing Wasp" 3 | slug: "install" 4 | date: "2017-06-01T15:00:31+08:00" 5 | --- 6 | 7 | We can't provide pre-built binaries currently, Wasp is written in Crystal with support for multiple platforms except Windows. 8 | 9 | ## Installing from source 10 | 11 | > Preqeuisite tools for downloading and building source code 12 | 13 | - [Git](http://git-scm.com/) 14 | - [Crystal](https://crystal-lang.org/) 0.26.0+ 15 | 16 | ``` 17 | $ git clone https://github.com/icyleaf/wasp.git && cd wasp 18 | $ make build 19 | $ sudo cp bin/wasp /usr/local/bin/wasp 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/layouts/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ site.title }} 11 | 12 | {% include "partial/header.html" %} 13 | 14 | 15 |
16 | {% include "partial/menu.html" %} 17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |

404

25 |

Page not found.

26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |

Are You Lost?

34 |

35 | The page you were looking for does not exist. If you think this is a mistake and one of our pages has gone missing. 36 |

37 |
38 |
39 |
40 |
41 | 42 | {% include "partial/footer.html" %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/layouts/_default/single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ page.title }} | {{ site.title }} 10 | 11 | {% include "partial/header.html" %} 12 | 13 | 14 | 15 |
16 | {% include "partial/menu.html" %} 17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |

{{ page.title }}

25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
{{ page.content | safe }}
34 |
35 |
36 |
37 |
38 | 39 | {% include "partial/footer.html" %} 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/layouts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ site.title }} 11 | 12 | {% include "partial/header.html" %} 13 | 14 | 15 | 20 | 21 |
22 |
23 |
24 |

25 |

{{ site.title }}

26 |
27 |
{{ site.description }}
28 |
29 |
30 | Get Started 31 |
32 |

33 | 34 |
35 |
36 |
Unsplashed background img 1
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |

flash_on

45 |
Speeds up development
46 | 47 |

We did most of the heavy lifting for you to provide a default stylings that incorporate our custom components. Additionally, we refined animations and transitions to provide a smoother experience for developers.

48 |
49 |
50 | 51 |
52 |
53 |

group

54 |
User Experience Focused
55 | 56 |

By utilizing elements and principles of Material Design, we were able to create a framework that incorporates components and animations that provide more feedback to users. Additionally, a single underlying responsive system across all platforms allow for a more unified user experience.

57 |
58 |
59 | 60 |
61 |
62 |

settings

63 |
Easy to work with
64 | 65 |

We have provided detailed documentation as well as specific code examples to help new users get started. We are also always open to feedback and can answer any questions a user may have about Materialize.

66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
{{ site.description }}
77 |
78 |
79 |
80 |
Unsplashed background img 2
81 |
82 | 83 |
84 |
85 | 86 |
87 |
88 |

89 |

Contributing

90 |
    91 |
  1. Fork it ( https://github.com/icyleaf/wasp/fork )
  2. 92 |
  3. Create your feature branch (git checkout -b my-new-feature)
  4. 93 |
  5. Commit your changes (git commit -am 'Add some feature')
  6. 94 |
  7. Push to the branch (git push origin my-new-feature)
  8. 95 |
  9. Create a new Pull Request
  10. 96 |
97 |
98 |
99 |
100 |
101 | 102 | 103 |
104 |
105 |
106 |
107 |
{{ site.description }}
108 |
109 |
110 |
111 |
Unsplashed background img 3
112 |
113 | 114 |
115 |
116 |
117 |
118 |
{{ site.title }}
119 |

{{ site.description }}

120 | 121 |
122 |
123 |
Inspires
124 | 129 |
130 |
131 |
Connect
132 | 137 |
138 |
139 |
140 | 146 |
147 | 148 | {% include "partial/footer.html" %} 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/layouts/partial/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/layouts/partial/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/layouts/partial/menu.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/static/background1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/background1.jpg -------------------------------------------------------------------------------- /docs/static/background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/background2.jpg -------------------------------------------------------------------------------- /docs/static/background3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/background3.jpg -------------------------------------------------------------------------------- /docs/static/css/style.css: -------------------------------------------------------------------------------- 1 | /* Custom Stylesheet */ 2 | /** 3 | * Use this file to override Materialize files so you can update 4 | * the core Materialize files in the future 5 | * 6 | * Made By MaterializeCSS.com 7 | */ 8 | 9 | nav ul a, 10 | nav .brand-logo { 11 | color: #444; 12 | } 13 | 14 | header, main { 15 | padding-left: 150px; 16 | } 17 | 18 | p { 19 | line-height: 2rem; 20 | color: rgba(0,0,0,0.71); 21 | } 22 | 23 | blockquote { 24 | color: rgba(0,0,0,0.71); 25 | border-left: 5px solid #f9a825; 26 | } 27 | 28 | .button-collapse { 29 | color: #26a69a; 30 | } 31 | 32 | .parallax-container { 33 | min-height: 380px; 34 | line-height: 0; 35 | height: auto; 36 | color: rgba(255,255,255,.9); 37 | } 38 | .parallax-container .section { 39 | width: 100%; 40 | } 41 | 42 | @media only screen and (max-width : 992px) { 43 | .parallax-container .section { 44 | position: absolute; 45 | top: 40%; 46 | } 47 | #index-banner .section { 48 | top: 10%; 49 | } 50 | } 51 | 52 | @media only screen and (max-width : 600px) { 53 | #index-banner .section { 54 | top: 0; 55 | } 56 | } 57 | 58 | /*#content-body h1, #content-body h2, #content-body h3, #content-body h4, #content-body h5 { 59 | color: #f9a825; 60 | }*/ 61 | 62 | #content-body p { 63 | font-size: 1.25rem; 64 | font-weight: 300; 65 | margin-bottom: 30px; 66 | } 67 | 68 | .icon-block { 69 | padding: 0 15px; 70 | } 71 | .icon-block .material-icons { 72 | font-size: inherit; 73 | } 74 | 75 | footer.page-footer { 76 | margin: 0; 77 | } -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Bold.woff -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Light.woff -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Light.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Medium.woff -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Regular.woff -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Thin.woff -------------------------------------------------------------------------------- /docs/static/fonts/roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icyleaf/wasp/2c0710547608a59b1f5fb9ced1384af258305916/docs/static/fonts/roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /docs/static/js/init.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(function(){ 3 | 4 | $('.button-collapse').sideNav(); 5 | $('.parallax').parallax(); 6 | 7 | }); // end of document ready 8 | })(jQuery); // end of jQuery name space -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | callback: 4 | github: mosop/callback 5 | version: 0.6.3 6 | 7 | cli: 8 | github: mosop/cli 9 | version: 0.7.0 10 | 11 | crinja: 12 | github: straight-shoota/crinja 13 | version: 0.3.0 14 | 15 | markd: 16 | github: icyleaf/markd 17 | version: 0.1.1 18 | 19 | optarg: 20 | github: mosop/optarg 21 | version: 0.5.8 22 | 23 | popcorn: 24 | github: icyleaf/popcorn 25 | version: 0.2.0 26 | 27 | string_inflection: 28 | github: mosop/string_inflection 29 | version: 0.2.1 30 | 31 | terminal-ui: 32 | github: icyleaf/terminal-ui.cr 33 | version: 0.1.2 34 | 35 | totem: 36 | github: icyleaf/totem 37 | commit: e122f16c77d6dcbaefc88b20f8d8e58a6b232760 38 | 39 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: wasp 2 | version: 0.1.1 3 | 4 | authors: 5 | - icyleaf 6 | 7 | targets: 8 | wasp: 9 | main: src/wasp.cr 10 | 11 | dependencies: 12 | markd: 13 | github: icyleaf/markd 14 | version: ~> 0.1.1 15 | totem: 16 | github: icyleaf/totem 17 | # version: ~> 0.4.0 18 | branch: master 19 | terminal-ui: 20 | github: icyleaf/terminal-ui.cr 21 | version: ~> 0.1.0 22 | crinja: 23 | github: straight-shoota/crinja 24 | version: ~> 0.3.0 25 | cli: 26 | github: mosop/cli 27 | version: ~> 0.7.0 28 | 29 | crystal: 0.26.1 30 | 31 | license: MIT 32 | -------------------------------------------------------------------------------- /spec/fixtures/configs/config.yml: -------------------------------------------------------------------------------- 1 | title: "Wasp" 2 | subtitle: "A Static Site Generator" 3 | description: "Wasp is a Static Site Generator written in Crystal." 4 | timezone: "Asia/Shanghai" 5 | 6 | base_url: "https://icyleaf.github.io/wasp/" 7 | # theme: "nest" 8 | 9 | permalink: ":section/:title/" 10 | ugly_url: false 11 | -------------------------------------------------------------------------------- /spec/fixtures/configs/config_with_social.yml: -------------------------------------------------------------------------------- 1 | title: "Wasp" 2 | subtitle: "A Static Site Generator" 3 | description: "Wasp is a Static Site Generator written in Crystal." 4 | timezone: "Asia/Shanghai" 5 | 6 | base_url: "https://icyleaf.github.io/wasp/" 7 | # theme: "nest" 8 | 9 | # avaiabled in basic/marked 10 | markdown: marked 11 | 12 | permalink: ":section/:title/" 13 | ugly_url: false 14 | 15 | social: 16 | twitter: icyleaf 17 | facebook: icyleaf 18 | instagram: icyleaf 19 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | ENV["WASP_SPEC_RUNNING"] = "true" 2 | 3 | require "spec" 4 | require "../src/wasp" 5 | -------------------------------------------------------------------------------- /spec/wasp/configuration_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private def load_config(file : String) 4 | path = File.expand_path("../../fixtures/configs", __FILE__) 5 | Wasp::Configuration.configure(File.join(path, file)) 6 | end 7 | 8 | describe Wasp::Configuration do 9 | describe "#initialize" do 10 | it "should returns same as YAML::Any" do 11 | config = load_config("config.yml") 12 | 13 | config["title"].should eq "Wasp" 14 | config["subtitle"].should eq "A Static Site Generator" 15 | config["description"].should eq "Wasp is a Static Site Generator written in Crystal." 16 | config["timezone"].should eq "Asia/Shanghai" 17 | config["base_url"].should eq "https://icyleaf.github.io/wasp/" 18 | config["permalink"].should eq ":section/:title/" 19 | config["ugly_url"].should be_false 20 | end 21 | end 22 | 23 | describe "#mapping" do 24 | it "should mapping full config to a struct" do 25 | config = load_config("config.yml") 26 | site = config.mapping(Wasp::Configuration::SiteStruct) 27 | site.title.should eq "Wasp" 28 | end 29 | 30 | it "should mapping a key of config to a struct" do 31 | config = load_config("config_with_social.yml") 32 | social = config.mapping(Wasp::Configuration::SocialStruct, "social") 33 | if s = social 34 | s.twitter.should eq "icyleaf" 35 | end 36 | end 37 | 38 | it "throws an exception if not exists the key" do 39 | config = load_config("config_with_social.yml") 40 | expect_raises Totem::MappingError do 41 | config.mapping(Wasp::Configuration::SocialStruct, "null") 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/wasp/filesystem/content_file_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Wasp::FileSystem::ContentFile do 4 | end 5 | -------------------------------------------------------------------------------- /spec/wasp/filesystem/front_matter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | private def empty_front_matter 4 | Wasp::FileSystem::FrontMatter.new("", "Asia/Shanghai") 5 | end 6 | 7 | describe Wasp::FileSystem::FrontMatter do 8 | describe "parse" do 9 | it "works with initialize with empty string" do 10 | empty_front_matter.should be_a(Wasp::FileSystem::FrontMatter) 11 | end 12 | 13 | it "raise exception without YAML data" do 14 | expect_raises Wasp::FrontMatterParseError do 15 | Wasp::FileSystem::FrontMatter.parse("not-yaml-data", "Asia/Shanghai") 16 | end 17 | end 18 | end 19 | 20 | describe "gets empty" do 21 | it "return empty with title" do 22 | m = empty_front_matter 23 | m.title.should eq("") 24 | end 25 | 26 | it "return empty with missing key" do 27 | m = empty_front_matter 28 | m.not_found_key.should eq("") 29 | end 30 | 31 | it "return unix time since 1970" do 32 | m = empty_front_matter 33 | m.date.should be_a(Time) 34 | m.date.year.should eq(1970) 35 | m.date.month.should eq(1) 36 | m.date.day.should eq(1) 37 | m.date.hour.should eq(0) 38 | m.date.minute.should eq(0) 39 | m.date.second.should eq(0) 40 | end 41 | end 42 | 43 | describe "gets" do 44 | it "should get each key from yaml data" do 45 | text = <<-YAML 46 | --- 47 | title: "Getting Started" 48 | slug: "getting-started" 49 | date: "2017-05-01T15:00:31+08:00" 50 | categories: Guide 51 | tags: 52 | - documents 53 | - install 54 | author: [icyleaf, "Wang Shen"] 55 | YAML 56 | 57 | m = Wasp::FileSystem::FrontMatter.new(text, "Asia/Shanghai") 58 | m.title.should eq("Getting Started") 59 | m.slug.should eq("getting-started") 60 | m.date.should eq(Time.parse("2017-05-01T15:00:31+08:00", Wasp::FileSystem::FrontMatter::WASP_DATE_FORMAT, Time::Location.load("Asia/Shanghai"))) 61 | m.categories.should eq(["Guide"]) 62 | m.tags.should eq(["documents", "install"]) 63 | m.draft?.should be_false 64 | 65 | m.author.should eq(["icyleaf", "Wang Shen"]) 66 | m.author.not_nil!.size.should eq(2) # because YAML::Type alias include Nil :( 67 | end 68 | 69 | it "tags accept string" do 70 | m = Wasp::FileSystem::FrontMatter.new("---\ntags: crystal", "Asia/Shanghai") 71 | m.tags.should eq(["crystal"]) 72 | end 73 | 74 | it "tags accept empty string" do 75 | m = Wasp::FileSystem::FrontMatter.new("---\ntags: ", "Asia/Shanghai") 76 | m.tags.should eq([] of String) 77 | end 78 | 79 | it "tags accept array" do 80 | m = Wasp::FileSystem::FrontMatter.new("---\ntags: \n - crystal\n - \"ruby\"", "Asia/Shanghai") 81 | m.tags.should eq(["crystal", "ruby"]) 82 | end 83 | 84 | it "tags returns empty without string or array" do 85 | m = Wasp::FileSystem::FrontMatter.new("---\ntags:\n crystal : Crystal", "Asia/Shanghai") 86 | m.tags.should eq([] of String) 87 | end 88 | 89 | it "categories accept string" do 90 | m = Wasp::FileSystem::FrontMatter.new("---\ncategories: crystal", "Asia/Shanghai") 91 | m.categories.should eq(["crystal"]) 92 | end 93 | 94 | it "categories accept empty string" do 95 | m = Wasp::FileSystem::FrontMatter.new("---\ncategories: ", "Asia/Shanghai") 96 | m.categories.should eq([] of String) 97 | end 98 | 99 | it "categories accept array" do 100 | m = Wasp::FileSystem::FrontMatter.new("---\ncategories: \n - crystal\n - \"ruby\"", "Asia/Shanghai") 101 | m.categories.should eq(["crystal", "ruby"]) 102 | end 103 | 104 | it "categories returns empty without string or array" do 105 | m = Wasp::FileSystem::FrontMatter.new("---\ncategories:\n crystal : Crystal", "Asia/Shanghai") 106 | m.categories.should eq([] of String) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/wasp_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Wasp do 4 | it "should has a name" do 5 | Wasp::NAME.should eq "Wasp" 6 | end 7 | 8 | it "should has a version" do 9 | Wasp::VERSION.should_not eq "" 10 | end 11 | 12 | it "should has a description" do 13 | Wasp::DESC.should_not eq "" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/wasp.cr: -------------------------------------------------------------------------------- 1 | require "./wasp/*" 2 | 3 | unless ENV.has_key?("WASP_SPEC_RUNNING") 4 | args = if ARGV.size > 0 5 | ARGV 6 | else 7 | %w(--help) 8 | end 9 | 10 | Wasp::Command.run args 11 | end 12 | -------------------------------------------------------------------------------- /src/wasp/command.cr: -------------------------------------------------------------------------------- 1 | require "cli" 2 | require "terminal-ui" 3 | 4 | module Wasp 5 | class GlobalOptions < Cli::Command 6 | class Options 7 | string %w(-s --source), var: "string", default: ".", desc: "the source path of site to read" 8 | string %w(-o --output), var: "string", desc: "the path of generate to write" 9 | bool "--verbose", default: false, desc: "verbose output" 10 | bool "--verboseLog", default: false, desc: "verbose logging timestamp" 11 | 12 | help 13 | end 14 | 15 | def run 16 | Terminal::UI.instance.logger.level = Logger::DEBUG if args.verbose? 17 | ENV["WASP_SHOW_TIMESTAMP"] = "true" if args.verboseLog? 18 | end 19 | end 20 | 21 | class Command < Cli::Supercommand 22 | class Help 23 | title "Wasp is a tool of static site generator, used to build your site." 24 | header "Complete documentation is available at http://github.com/icyleaf/wasp/." 25 | footer "Use 'wasp [command] --help' for more information about a command." 26 | end 27 | 28 | class Options 29 | string %w(-s --source), var: "string", desc: "the source path of site to read" 30 | string %w(-o --output), var: "string", desc: "the path of generate to write" 31 | bool "--verbose", default: false, desc: "verbose output" 32 | bool "--verboseLog", default: false, desc: "verbose logging timestamp" 33 | 34 | help 35 | end 36 | end 37 | end 38 | 39 | require "./commands/*" 40 | -------------------------------------------------------------------------------- /src/wasp/commands/build.cr: -------------------------------------------------------------------------------- 1 | require "file_utils" 2 | require "yaml" 3 | 4 | module Wasp 5 | class Command < Cli::Supercommand 6 | command "b", aliased: "build" 7 | 8 | class Build < GlobalOptions 9 | class Help 10 | caption "Build markdown to static files" 11 | end 12 | 13 | class Options 14 | string %w(-b --baseURL), var: "string", desc: "hostname (and path) to the root, e.g. http://icyleaf.com/" 15 | help 16 | end 17 | 18 | def run 19 | elapsed_time do 20 | options = Hash(String, String).new.tap do |obj| 21 | obj["base_url"] = args.baseURL if args.baseURL? 22 | end 23 | 24 | generator = Wasp::Generator.new args.source, options 25 | Terminal::UI.verbose "Using config file: #{generator.context.source_path}/config.yml" 26 | Terminal::UI.verbose "Generating static files to #{generator.context.public_path}" 27 | generator.run 28 | end 29 | end 30 | 31 | private def elapsed_time 32 | started_at = Time.now 33 | yield 34 | Terminal::UI.message("Total in #{(Time.now - started_at).total_milliseconds} ms") 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/wasp/commands/config.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Command < Cli::Supercommand 3 | command "c", aliased: "config" 4 | 5 | class Config < GlobalOptions 6 | class Help 7 | caption "Print site configuration" 8 | end 9 | 10 | def run 11 | path = args.source? ? args.source : "." 12 | config = Configuration.configure(path) 13 | config.each do |k, v| 14 | puts "#{k}: #{v}" 15 | end 16 | rescue e : NotFoundFileError 17 | Terminal::UI.error e.message 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/wasp/commands/init.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Command < Cli::Supercommand 3 | command "i", aliased: "init" 4 | 5 | class Init < GlobalOptions 6 | class Help 7 | caption "Initialize a new site" 8 | end 9 | 10 | def run 11 | super 12 | 13 | Terminal::UI.important "To be continue" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/wasp/commands/new.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Command < Cli::Supercommand 3 | command "n", aliased: "new" 4 | 5 | class New < GlobalOptions 6 | class Help 7 | caption "Create a new content(post, page etc)" 8 | end 9 | 10 | def run 11 | super 12 | 13 | Terminal::UI.important "To be continue" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/wasp/commands/server.cr: -------------------------------------------------------------------------------- 1 | require "http/server" 2 | require "../server/handlers/*" 3 | 4 | module Wasp 5 | class Command < Cli::Supercommand 6 | command "s", aliased: "server" 7 | 8 | class Server < GlobalOptions 9 | class Help 10 | caption "Run a web server" 11 | end 12 | 13 | class Options 14 | string %w(-b --baseURL), var: "string", default: "/", desc: "hostname (and path) to the root, e.g. http://icyleaf.com/" 15 | string "--bindHost", var: "string", default: "127.0.0.1", desc: "interface to which the server will bind" 16 | string "--watchInterval", var: "int", default: "1", desc: "seconds to wait between watch filesystem" 17 | string %w(-p --port), var: "int", default: "8624", desc: "port on which the server will listen" 18 | bool %w(-w --watch), not: "-W", default: true, desc: "watch filesystem for changes and recreate as needed" 19 | help 20 | end 21 | 22 | def run 23 | super 24 | 25 | # TODO: it does not works with localhost:8624 in HTTP::Server 26 | base_url = args.baseURL? ? args.baseURL : "/" 27 | build_args = ["--source", args.source, "--baseURL", base_url] 28 | Build.run(build_args) 29 | 30 | port = args.port 31 | public_path = if args.source? 32 | File.join(args.source, "public") 33 | else 34 | "public" 35 | end 36 | 37 | handlers = [ 38 | Wasp::StaticSiteHandler.new(public_path), 39 | ] of HTTP::Handler 40 | 41 | if args.watch? 42 | source_path = File.expand_path(args.source) 43 | watcher = Watcher.new(source_path) 44 | 45 | puts "" 46 | Terminal::UI.verbose "Watch changes in '#{source_path}/{#{watcher.rules.join(",")}}'" 47 | 48 | livereload_hanlder = Wasp::LiveReloadHandler.new(public_path, port.to_i) do |ws| 49 | ws.on_message do |message| 50 | if message.includes?("\"command\":\"hello\"") 51 | ws.send({ 52 | "command" => "hello", 53 | "protocols" => [ 54 | "http://livereload.com/protocols/official-7", 55 | ], 56 | "serverName": "Wasp", 57 | }.to_json) 58 | end 59 | end 60 | 61 | spawn do 62 | loop do 63 | watcher.watch_changes do |file, status| 64 | Terminal::UI.message "File #{status}: #{file}" 65 | Build.run(build_args) 66 | 67 | ws.send({ 68 | "command" => "reload", 69 | "path": file, 70 | "liveCSS": true, 71 | }.to_json) 72 | end 73 | 74 | sleep args.watchInterval.to_i 75 | end 76 | end 77 | end 78 | 79 | handlers.insert(0, livereload_hanlder) 80 | end 81 | 82 | Terminal::UI.message "Web Server is running at http://localhost:#{args.port}/ (bind address #{args.bindHost})" 83 | Terminal::UI.message "Press Ctrl+C to stop" 84 | server = HTTP::Server.new(handlers: handlers) 85 | server.listen args.bindHost, port.to_i 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /src/wasp/commands/version.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Command < Cli::Supercommand 3 | command "v", aliased: "version" 4 | 5 | class Version < GlobalOptions 6 | class Help 7 | caption "Print version of Wasp" 8 | end 9 | 10 | def run 11 | super 12 | 13 | Terminal::UI.message "#{Wasp::NAME} - #{Wasp::DESC} v#{Wasp::VERSION} in Crystal v#{Crystal::VERSION}" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/wasp/configuration.cr: -------------------------------------------------------------------------------- 1 | require "totem" 2 | 3 | module Wasp 4 | class Configuration 5 | include Totem::ConfigBuilder 6 | 7 | build do 8 | config_type "yaml" 9 | end 10 | 11 | def app_info 12 | { 13 | "name": Wasp::NAME, 14 | "version": Wasp::VERSION, 15 | "crystal": Crystal::VERSION, 16 | } 17 | end 18 | 19 | struct SiteStruct 20 | include YAML::Serializable 21 | 22 | property title : String 23 | property subtitle : String 24 | property description : String 25 | property base_url : String 26 | property timezone : String 27 | property permalink : String 28 | property ugly_url : Bool 29 | end 30 | 31 | struct SocialStruct 32 | include YAML::Serializable 33 | 34 | property twitter : String 35 | property facebook : String 36 | property instagram : String 37 | end 38 | 39 | struct AppStruct 40 | property name : String = Wasp::NAME 41 | property version : String = Wasp::VERSION 42 | property crystal : String = Crystal::VERSION 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/wasp/exception.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Error < Exception; end 3 | 4 | class NotFoundFileError < Error; end 5 | 6 | class ConfigDecodeError < Error; end 7 | 8 | class FrontMatterParseError < Error; end 9 | 10 | class MissingFrontMatterError < Error; end 11 | end 12 | -------------------------------------------------------------------------------- /src/wasp/filesystem.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class FileSystem 3 | end 4 | end 5 | 6 | require "./filesystem/*" 7 | -------------------------------------------------------------------------------- /src/wasp/filesystem/content_file.cr: -------------------------------------------------------------------------------- 1 | require "./front_matter" 2 | require "markd" 3 | require "uri" 4 | 5 | class Wasp::FileSystem 6 | struct ContentFile 7 | getter content 8 | 9 | @content : String 10 | 11 | FRONT_MATTER_REGEX = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m 12 | 13 | def initialize(file : String, @site_config : Configuration) 14 | @file = File.expand_path(file) 15 | @name = File.basename(@file) 16 | 17 | text = File.read(@file) 18 | if text =~ FRONT_MATTER_REGEX 19 | @front_matter = FrontMatter.new($1, @site_config["timezone"].as_s) 20 | else 21 | raise MissingFrontMatterError.new("Not found metadata in " + @file) 22 | end 23 | 24 | @content = Markd.to_html(text.gsub(FRONT_MATTER_REGEX, "")) 25 | end 26 | 27 | def section 28 | sections = @file.split("/") 29 | start_index = sections.index("contents").not_nil! 30 | sections.delete_at(start_index + 1, (sections.size - start_index - 2)).join("/") 31 | end 32 | 33 | def filename 34 | @name.chomp(File.extname(@name)) 35 | end 36 | 37 | def summary(limit = 300) 38 | # TODO: process to pure text 39 | @content[0..limit] + " »" 40 | end 41 | 42 | def link(ugly_url = "false") 43 | ugly_url = @site_config["ugly_url"]? || ugly_url.to_s 44 | File.join(@site_config["base_url"].to_s, permalink(ugly_url)) 45 | end 46 | 47 | def permalink(ugly_url = "false") 48 | sections = [] of String 49 | (@site_config["permalink"]? || ":filename").to_s.split("/").each do |section| 50 | next if section.empty? 51 | 52 | sections << permalink_section(section).to_s 53 | end 54 | 55 | uri = sections.join("/") 56 | uri = uri + ".html" if ugly_url == "true" 57 | uri 58 | end 59 | 60 | forward_missing_to @front_matter 61 | 62 | def to_h 63 | @front_matter.to_h.merge({ 64 | "summary" => Totem::Any.new(summary), 65 | "content" => Totem::Any.new(@content), 66 | "permalink" => Totem::Any.new(permalink), 67 | "link" => Totem::Any.new(link), 68 | }) 69 | end 70 | 71 | private def permalink_section(uri) 72 | return "" if !uri || uri.empty? 73 | 74 | case uri 75 | when ":year" 76 | @front_matter.date.year 77 | when ":month" 78 | @front_matter.date.month 79 | when ":day" 80 | @front_matter.date.day 81 | when ":title", ":slug" 82 | @front_matter.slug ? @front_matter.slug : URI.escape(@front_matter.title.downcase.gsub(" ", "-")) 83 | when ":section" 84 | section 85 | else 86 | # such as :filename or others 87 | filename 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /src/wasp/filesystem/front_matter.cr: -------------------------------------------------------------------------------- 1 | class Wasp::FileSystem 2 | struct FrontMatter 3 | WASP_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%:z" 4 | 5 | @inner : Totem::Config 6 | 7 | def self.parse(text : String, timezone : String) 8 | self.new(text, timezone) 9 | end 10 | 11 | def initialize(text : String, @timezone : String) 12 | @inner = if text.empty? 13 | Totem.new 14 | else 15 | begin 16 | Totem.from_yaml(text) 17 | rescue TypeCastError 18 | raise FrontMatterParseError.new("can not parse front matter from yaml string") 19 | end 20 | end 21 | end 22 | 23 | def title 24 | @inner["title"]?.to_s 25 | end 26 | 27 | def date 28 | Time.parse(@inner.fetch("date", "1970-01-01T00:00:00+00:00").to_s, WASP_DATE_FORMAT, Time::Location.load(@timezone)) 29 | end 30 | 31 | def slug 32 | @inner["slug"]?.to_s 33 | end 34 | 35 | def tags 36 | find_array_value("tags") 37 | end 38 | 39 | def categories 40 | find_array_value("categories") 41 | end 42 | 43 | def draft? 44 | @inner.fetch("draft", "false").as_bool 45 | end 46 | 47 | def to_h 48 | @inner.set_defaults({ 49 | "date" => date, 50 | "tags" => tags, 51 | "categories" => categories, 52 | }) 53 | 54 | @inner.to_h 55 | end 56 | 57 | def dup 58 | end 59 | 60 | forward_missing_to @inner 61 | 62 | macro method_missing(call) 63 | @inner.fetch({{ call.name.id.stringify }}, "") 64 | 65 | # TODO: i don't know why this not works 66 | # case object = @inner.fetch({{ call.name.id.stringify }}, "") 67 | # when Nil 68 | # object.to_s 69 | # when String 70 | # object.as(YAML::Any) 71 | # when Array 72 | # puts {{ call.name.id.stringify }} 73 | # puts object.class 74 | # object.as(Array(YAML::Any)) 75 | # when Hash 76 | # object.as(Hash(YAML::Any, YAML::Any)) 77 | # else 78 | # object 79 | # end 80 | end 81 | 82 | private def find_array_value(key : String) 83 | empty_array = Array(String).new 84 | return empty_array unless object = @inner[key]? 85 | 86 | case object 87 | when .as_s? 88 | [object.as_s] 89 | when .as_a? 90 | object.as_a.map(&.as_s) 91 | else 92 | empty_array 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /src/wasp/generator.cr: -------------------------------------------------------------------------------- 1 | require "./generator/*" 2 | 3 | module Wasp 4 | class Generator 5 | getter context : Context 6 | 7 | @handlers : Array(Handler) 8 | 9 | def initialize(source_path : String, options = {} of String => String, handlers = [] of Handler) 10 | @context = Context.new source_path, options 11 | @handlers = default_handers.concat handlers 12 | end 13 | 14 | def run 15 | 0.upto(@handlers.size - 2) { |i| @handlers[i].next = @handlers[i + 1] } 16 | @handlers.first.call @context 17 | end 18 | 19 | private def default_handers 20 | [Contents.new.as(Handler)] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/wasp/generator/context.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Generator 3 | class Context 4 | CONTENTS_PATH = "contents" 5 | LAYOUTS_PATH = "layouts" 6 | PUBLIC_PATH = "public" 7 | STATIC_PATH = "static" 8 | 9 | property source_path : String 10 | property site_config : Configuration 11 | # property fs : FileSystem 12 | property pages : Array(FileSystem::ContentFile) 13 | 14 | def initialize(@source_path : String, options = {} of String => String) 15 | @site_config = load_and_merge_config(@source_path, options) 16 | @pages = pages 17 | end 18 | 19 | def app_info 20 | { 21 | "name" => Wasp::NAME, 22 | "version" => Wasp::VERSION, 23 | } 24 | end 25 | 26 | def pages 27 | Array(FileSystem::ContentFile).new.tap do |files| 28 | Dir.glob(File.join(contents_path, "**", "*.md")).each do |file| 29 | files << FileSystem::ContentFile.new(file, @site_config) 30 | end 31 | end.sort_by(&.date).reverse 32 | end 33 | 34 | {% for method in @type.constants %} 35 | def {{ method.stringify.downcase.id }} 36 | path_to({{ method.id }}) 37 | end 38 | {% end %} 39 | 40 | private def path_to(path) 41 | File.join(@source_path, path) 42 | end 43 | 44 | private def load_and_merge_config(source_path : String, options : Hash(String, String)) 45 | config = Configuration.configure(source_path) 46 | options.each do |k, v| 47 | config.set(k, v) 48 | end 49 | config 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/wasp/generator/handler.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Generator 3 | module Handler 4 | property next : Handler | Nil 5 | 6 | abstract def call(context : Wasp::Generator::Context) 7 | 8 | def call_next(context : Wasp::Generator::Context) 9 | if next_handler = @next 10 | next_handler.call context 11 | end 12 | end 13 | end 14 | end 15 | end 16 | 17 | require "./handlers/*" 18 | -------------------------------------------------------------------------------- /src/wasp/generator/handlers/contents.cr: -------------------------------------------------------------------------------- 1 | require "file_utils" 2 | require "crinja" 3 | 4 | class Wasp::Generator 5 | class Contents 6 | include Handler 7 | 8 | PAGES = ["index.html", "404.html"] 9 | 10 | def call(context) 11 | generate_assets context 12 | generate_pages context 13 | 14 | call_next context 15 | end 16 | 17 | private def generate_assets(context) 18 | public_path = context.public_path 19 | 20 | source_static_path = context.static_path 21 | public_static_path = File.join public_path, Context::STATIC_PATH 22 | 23 | FileUtils.rm_rf(public_path) if Dir.exists?(public_path) 24 | FileUtils.mkdir_p public_path 25 | FileUtils.cp_r source_static_path, public_static_path 26 | end 27 | 28 | private def generate_pages(context) 29 | template_env = Crinja.new 30 | template_env.loader = Crinja::Loader::FileSystemLoader.new(context.layouts_path) 31 | 32 | variables = { 33 | "site" => context.site_config.to_h, 34 | "wasp" => context.app_info, 35 | "pages" => context.pages.map(&.to_h), 36 | } 37 | 38 | PAGES.each do |page| 39 | desc_file = File.join context.public_path, page 40 | generate_page page, desc_file, template_env, variables 41 | end 42 | 43 | context.pages.each do |content| 44 | source_file = "_default/single.html" 45 | desc_file = File.join(context.public_path, content.permalink, "index.html") 46 | 47 | generate_page(source_file, desc_file, template_env, variables) do |variables| 48 | Terminal::UI.verbose("Write to #{desc_file}") 49 | 50 | variables["page"] = content.to_h 51 | variables 52 | end 53 | end 54 | end 55 | 56 | private def generate_page(source_file, desc_file, template_env, variables) 57 | template = template_env.get_template source_file 58 | File.write desc_file, template.render(variables) 59 | end 60 | 61 | private def generate_page(source_file, desc_file, template_env, variables) 62 | template = template_env.get_template source_file 63 | variables = yield variables 64 | 65 | desc_path = File.dirname desc_file 66 | FileUtils.mkdir_p desc_path unless Dir.exists?(desc_path) 67 | 68 | File.write desc_file, template.render(variables) 69 | end 70 | 71 | private def template_file(path) 72 | file = File.open(path) 73 | Liquid::Template.parse(file) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/wasp/server/handlers/livereload_handler.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class LiveReloadHandler < HTTP::WebSocketHandler 3 | def initialize(public_path : String, @port : Int32, &@proc : HTTP::WebSocket, HTTP::Server::Context ->) 4 | @path = "/livereload" 5 | @public_path = File.expand_path(public_path) 6 | # @watcher = Watcher.new() 7 | 8 | # proc = ->(socket : HTTP::WebSocket, HTTP::Server::Context ->) { 9 | # puts typeof(socket) 10 | 11 | # # watch_changes(socket) 12 | 13 | # # socket.on_close do 14 | # # puts "Server: Closing socket" 15 | # # end 16 | # } 17 | 18 | # sssproc = ->(){} 19 | # super(@path, ) 20 | end 21 | 22 | def call(context) 23 | original_path = context.request.path.not_nil! 24 | is_dir_path = original_path.ends_with?("/") 25 | request_path = URI.unescape(original_path) 26 | 27 | # Load livereload js 28 | if request_path == "/livereload.js" 29 | js_file = File.expand_path("../../static/livereload.js", __FILE__) 30 | return File.open(js_file) do |file| 31 | IO.copy(file, context.response) 32 | end 33 | end 34 | 35 | # inject \n" 45 | 46 | if File.exists?(file_path) 47 | content = File.read(file_path) 48 | context.response.content_type = "text/html" 49 | context.response.content_length = File.size(file_path) 50 | return context.response.print content.gsub(end_body_tag, liverelaod_template) 51 | end 52 | end 53 | 54 | return call_next(context) unless context.request.path.not_nil! == @path 55 | super 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/wasp/server/handlers/static_site_handler.cr: -------------------------------------------------------------------------------- 1 | require "uri" 2 | 3 | module Wasp 4 | class StaticSiteHandler < HTTP::StaticFileHandler 5 | def call(context) 6 | unless context.request.method == "GET" || context.request.method == "HEAD" 7 | if @fallthrough 8 | call_next(context) 9 | else 10 | context.response.status_code = 405 11 | context.response.headers.add("Allow", "GET, HEAD") 12 | end 13 | return 14 | end 15 | 16 | original_path = context.request.path.not_nil! 17 | is_dir_path = original_path.ends_with? "/" 18 | request_path = self.request_path(URI.unescape(original_path)) 19 | 20 | expanded_path = File.expand_path(request_path, "/") 21 | if is_dir_path && !expanded_path.ends_with? "/" 22 | expanded_path = "#{expanded_path}/" 23 | end 24 | is_dir_path = expanded_path.ends_with? "/" 25 | 26 | file_path = File.join(@public_dir, expanded_path) 27 | is_dir = Dir.exists? file_path 28 | 29 | if request_path != expanded_path || is_dir && !is_dir_path 30 | redirect_to context, "#{expanded_path}#{is_dir && !is_dir_path ? "/" : ""}" 31 | return 32 | end 33 | 34 | # File path cannot contains '\0' (NUL) because all filesystem I know 35 | # don't accept '\0' character as file name. 36 | if request_path.includes? '\0' 37 | context.response.status_code = 400 38 | return 39 | end 40 | 41 | if Dir.exists?(file_path) 42 | file_path = File.join(file_path, "index.html") 43 | end 44 | 45 | if File.exists?(file_path) 46 | html_response(context, file_path) 47 | elsif File.exists?(File.join(@public_dir, "404.html")) 48 | html_response(context, File.join(@public_dir, "404.html")) 49 | else 50 | call_next(context) 51 | end 52 | end 53 | 54 | private def html_response(context, html_file) 55 | context.response.content_type = mime_type(html_file) 56 | context.response.content_length = File.size(html_file) 57 | File.open(html_file) do |file| 58 | IO.copy(file, context.response) 59 | end 60 | end 61 | 62 | private def mime_type(path) 63 | case File.extname(path) 64 | when ".txt" then "text/plain" 65 | when ".htm", ".html" then "text/html" 66 | when ".css" then "text/css" 67 | when ".jpg", ".jpeg" then "image/jpeg" 68 | when ".gif" then "image/gif" 69 | when ".png" then "image/png" 70 | when ".js" then "application/javascript" 71 | else "application/octet-stream" 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /src/wasp/server/static/livereload.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); 322 | return; 323 | } 324 | } 325 | this.reloader = new Reloader(this.window, this.console, Timer); 326 | this.connector = new Connector(this.options, this.WebSocket, Timer, { 327 | connecting: (function(_this) { 328 | return function() {}; 329 | })(this), 330 | socketConnected: (function(_this) { 331 | return function() {}; 332 | })(this), 333 | connected: (function(_this) { 334 | return function(protocol) { 335 | var _base; 336 | if (typeof (_base = _this.listeners).connect === "function") { 337 | _base.connect(); 338 | } 339 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); 340 | return _this.analyze(); 341 | }; 342 | })(this), 343 | error: (function(_this) { 344 | return function(e) { 345 | if (e instanceof ProtocolError) { 346 | if (typeof console !== "undefined" && console !== null) { 347 | return console.log("" + e.message + "."); 348 | } 349 | } else { 350 | if (typeof console !== "undefined" && console !== null) { 351 | return console.log("LiveReload internal error: " + e.message); 352 | } 353 | } 354 | }; 355 | })(this), 356 | disconnected: (function(_this) { 357 | return function(reason, nextDelay) { 358 | var _base; 359 | if (typeof (_base = _this.listeners).disconnect === "function") { 360 | _base.disconnect(); 361 | } 362 | switch (reason) { 363 | case 'cannot-connect': 364 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); 365 | case 'broken': 366 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); 367 | case 'handshake-timeout': 368 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); 369 | case 'handshake-failed': 370 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); 371 | case 'manual': 372 | break; 373 | case 'error': 374 | break; 375 | default: 376 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); 377 | } 378 | }; 379 | })(this), 380 | message: (function(_this) { 381 | return function(message) { 382 | switch (message.command) { 383 | case 'reload': 384 | return _this.performReload(message); 385 | case 'alert': 386 | return _this.performAlert(message); 387 | } 388 | }; 389 | })(this) 390 | }); 391 | this.initialized = true; 392 | } 393 | 394 | LiveReload.prototype.on = function(eventName, handler) { 395 | return this.listeners[eventName] = handler; 396 | }; 397 | 398 | LiveReload.prototype.log = function(message) { 399 | return this.console.log("" + message); 400 | }; 401 | 402 | LiveReload.prototype.performReload = function(message) { 403 | var _ref, _ref1; 404 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); 405 | return this.reloader.reload(message.path, { 406 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true, 407 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, 408 | originalPath: message.originalPath || '', 409 | overrideURL: message.overrideURL || '', 410 | serverURL: "http://" + this.options.host + ":" + this.options.port 411 | }); 412 | }; 413 | 414 | LiveReload.prototype.performAlert = function(message) { 415 | return alert(message.message); 416 | }; 417 | 418 | LiveReload.prototype.shutDown = function() { 419 | var _base; 420 | if (!this.initialized) { 421 | return; 422 | } 423 | this.connector.disconnect(); 424 | this.log("LiveReload disconnected."); 425 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; 426 | }; 427 | 428 | LiveReload.prototype.hasPlugin = function(identifier) { 429 | return !!this.pluginIdentifiers[identifier]; 430 | }; 431 | 432 | LiveReload.prototype.addPlugin = function(pluginClass) { 433 | var plugin; 434 | if (!this.initialized) { 435 | return; 436 | } 437 | if (this.hasPlugin(pluginClass.identifier)) { 438 | return; 439 | } 440 | this.pluginIdentifiers[pluginClass.identifier] = true; 441 | plugin = new pluginClass(this.window, { 442 | _livereload: this, 443 | _reloader: this.reloader, 444 | _connector: this.connector, 445 | console: this.console, 446 | Timer: Timer, 447 | generateCacheBustUrl: (function(_this) { 448 | return function(url) { 449 | return _this.reloader.generateCacheBustUrl(url); 450 | }; 451 | })(this) 452 | }); 453 | this.plugins.push(plugin); 454 | this.reloader.addPlugin(plugin); 455 | }; 456 | 457 | LiveReload.prototype.analyze = function() { 458 | var plugin, pluginData, pluginsData, _i, _len, _ref; 459 | if (!this.initialized) { 460 | return; 461 | } 462 | if (!(this.connector.protocol >= 7)) { 463 | return; 464 | } 465 | pluginsData = {}; 466 | _ref = this.plugins; 467 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 468 | plugin = _ref[_i]; 469 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; 470 | pluginData.version = plugin.constructor.version; 471 | } 472 | this.connector.sendCommand({ 473 | command: 'info', 474 | plugins: pluginsData, 475 | url: this.window.location.href 476 | }); 477 | }; 478 | 479 | return LiveReload; 480 | 481 | })(); 482 | 483 | }).call(this); 484 | 485 | },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ 486 | (function() { 487 | var Options; 488 | 489 | exports.Options = Options = (function() { 490 | function Options() { 491 | this.https = false; 492 | this.host = null; 493 | this.port = 35729; 494 | this.snipver = null; 495 | this.ext = null; 496 | this.extver = null; 497 | this.mindelay = 1000; 498 | this.maxdelay = 60000; 499 | this.handshake_timeout = 5000; 500 | } 501 | 502 | Options.prototype.set = function(name, value) { 503 | if (typeof value === 'undefined') { 504 | return; 505 | } 506 | if (!isNaN(+value)) { 507 | value = +value; 508 | } 509 | return this[name] = value; 510 | }; 511 | 512 | return Options; 513 | 514 | })(); 515 | 516 | Options.extract = function(document) { 517 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; 518 | _ref = document.getElementsByTagName('script'); 519 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 520 | element = _ref[_i]; 521 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { 522 | options = new Options(); 523 | options.https = src.indexOf("https") === 0; 524 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { 525 | options.host = mm[1]; 526 | if (mm[2]) { 527 | options.port = parseInt(mm[2], 10); 528 | } 529 | } 530 | if (m[2]) { 531 | _ref1 = m[2].split('&'); 532 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 533 | pair = _ref1[_j]; 534 | if ((keyAndValue = pair.split('=')).length > 1) { 535 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); 536 | } 537 | } 538 | } 539 | return options; 540 | } 541 | } 542 | return null; 543 | }; 544 | 545 | }).call(this); 546 | 547 | },{}],6:[function(require,module,exports){ 548 | (function() { 549 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, 550 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 551 | 552 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; 553 | 554 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; 555 | 556 | exports.ProtocolError = ProtocolError = (function() { 557 | function ProtocolError(reason, data) { 558 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; 559 | } 560 | 561 | return ProtocolError; 562 | 563 | })(); 564 | 565 | exports.Parser = Parser = (function() { 566 | function Parser(handlers) { 567 | this.handlers = handlers; 568 | this.reset(); 569 | } 570 | 571 | Parser.prototype.reset = function() { 572 | return this.protocol = null; 573 | }; 574 | 575 | Parser.prototype.process = function(data) { 576 | var command, e, message, options, _ref; 577 | try { 578 | if (this.protocol == null) { 579 | if (data.match(/^!!ver:([\d.]+)$/)) { 580 | this.protocol = 6; 581 | } else if (message = this._parseMessage(data, ['hello'])) { 582 | if (!message.protocols.length) { 583 | throw new ProtocolError("no protocols specified in handshake message"); 584 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { 585 | this.protocol = 7; 586 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { 587 | this.protocol = 6; 588 | } else { 589 | throw new ProtocolError("no supported protocols found"); 590 | } 591 | } 592 | return this.handlers.connected(this.protocol); 593 | } else if (this.protocol === 6) { 594 | message = JSON.parse(data); 595 | if (!message.length) { 596 | throw new ProtocolError("protocol 6 messages must be arrays"); 597 | } 598 | command = message[0], options = message[1]; 599 | if (command !== 'refresh') { 600 | throw new ProtocolError("unknown protocol 6 command"); 601 | } 602 | return this.handlers.message({ 603 | command: 'reload', 604 | path: options.path, 605 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true 606 | }); 607 | } else { 608 | message = this._parseMessage(data, ['reload', 'alert']); 609 | return this.handlers.message(message); 610 | } 611 | } catch (_error) { 612 | e = _error; 613 | if (e instanceof ProtocolError) { 614 | return this.handlers.error(e); 615 | } else { 616 | throw e; 617 | } 618 | } 619 | }; 620 | 621 | Parser.prototype._parseMessage = function(data, validCommands) { 622 | var e, message, _ref; 623 | try { 624 | message = JSON.parse(data); 625 | } catch (_error) { 626 | e = _error; 627 | throw new ProtocolError('unparsable JSON', data); 628 | } 629 | if (!message.command) { 630 | throw new ProtocolError('missing "command" key', data); 631 | } 632 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { 633 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); 634 | } 635 | return message; 636 | }; 637 | 638 | return Parser; 639 | 640 | })(); 641 | 642 | }).call(this); 643 | 644 | },{}],7:[function(require,module,exports){ 645 | (function() { 646 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; 647 | 648 | splitUrl = function(url) { 649 | var hash, index, params; 650 | if ((index = url.indexOf('#')) >= 0) { 651 | hash = url.slice(index); 652 | url = url.slice(0, index); 653 | } else { 654 | hash = ''; 655 | } 656 | if ((index = url.indexOf('?')) >= 0) { 657 | params = url.slice(index); 658 | url = url.slice(0, index); 659 | } else { 660 | params = ''; 661 | } 662 | return { 663 | url: url, 664 | params: params, 665 | hash: hash 666 | }; 667 | }; 668 | 669 | pathFromUrl = function(url) { 670 | var path; 671 | url = splitUrl(url).url; 672 | if (url.indexOf('file://') === 0) { 673 | path = url.replace(/^file:\/\/(localhost)?/, ''); 674 | } else { 675 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); 676 | } 677 | return decodeURIComponent(path); 678 | }; 679 | 680 | pickBestMatch = function(path, objects, pathFunc) { 681 | var bestMatch, object, score, _i, _len; 682 | bestMatch = { 683 | score: 0 684 | }; 685 | for (_i = 0, _len = objects.length; _i < _len; _i++) { 686 | object = objects[_i]; 687 | score = numberOfMatchingSegments(path, pathFunc(object)); 688 | if (score > bestMatch.score) { 689 | bestMatch = { 690 | object: object, 691 | score: score 692 | }; 693 | } 694 | } 695 | if (bestMatch.score > 0) { 696 | return bestMatch; 697 | } else { 698 | return null; 699 | } 700 | }; 701 | 702 | numberOfMatchingSegments = function(path1, path2) { 703 | var comps1, comps2, eqCount, len; 704 | path1 = path1.replace(/^\/+/, '').toLowerCase(); 705 | path2 = path2.replace(/^\/+/, '').toLowerCase(); 706 | if (path1 === path2) { 707 | return 10000; 708 | } 709 | comps1 = path1.split('/').reverse(); 710 | comps2 = path2.split('/').reverse(); 711 | len = Math.min(comps1.length, comps2.length); 712 | eqCount = 0; 713 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { 714 | ++eqCount; 715 | } 716 | return eqCount; 717 | }; 718 | 719 | pathsMatch = function(path1, path2) { 720 | return numberOfMatchingSegments(path1, path2) > 0; 721 | }; 722 | 723 | IMAGE_STYLES = [ 724 | { 725 | selector: 'background', 726 | styleNames: ['backgroundImage'] 727 | }, { 728 | selector: 'border', 729 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] 730 | } 731 | ]; 732 | 733 | exports.Reloader = Reloader = (function() { 734 | function Reloader(window, console, Timer) { 735 | this.window = window; 736 | this.console = console; 737 | this.Timer = Timer; 738 | this.document = this.window.document; 739 | this.importCacheWaitPeriod = 200; 740 | this.plugins = []; 741 | } 742 | 743 | Reloader.prototype.addPlugin = function(plugin) { 744 | return this.plugins.push(plugin); 745 | }; 746 | 747 | Reloader.prototype.analyze = function(callback) { 748 | return results; 749 | }; 750 | 751 | Reloader.prototype.reload = function(path, options) { 752 | var plugin, _base, _i, _len, _ref; 753 | this.options = options; 754 | if ((_base = this.options).stylesheetReloadTimeout == null) { 755 | _base.stylesheetReloadTimeout = 15000; 756 | } 757 | _ref = this.plugins; 758 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 759 | plugin = _ref[_i]; 760 | if (plugin.reload && plugin.reload(path, options)) { 761 | return; 762 | } 763 | } 764 | if (options.liveCSS) { 765 | if (path.match(/\.css$/i)) { 766 | if (this.reloadStylesheet(path)) { 767 | return; 768 | } 769 | } 770 | } 771 | if (options.liveImg) { 772 | if (path.match(/\.(jpe?g|png|gif)$/i)) { 773 | this.reloadImages(path); 774 | return; 775 | } 776 | } 777 | return this.reloadPage(); 778 | }; 779 | 780 | Reloader.prototype.reloadPage = function() { 781 | return this.window.document.location.reload(); 782 | }; 783 | 784 | Reloader.prototype.reloadImages = function(path) { 785 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; 786 | expando = this.generateUniqueString(); 787 | _ref = this.document.images; 788 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 789 | img = _ref[_i]; 790 | if (pathsMatch(path, pathFromUrl(img.src))) { 791 | img.src = this.generateCacheBustUrl(img.src, expando); 792 | } 793 | } 794 | if (this.document.querySelectorAll) { 795 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 796 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; 797 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); 798 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 799 | img = _ref2[_k]; 800 | this.reloadStyleImages(img.style, styleNames, path, expando); 801 | } 802 | } 803 | } 804 | if (this.document.styleSheets) { 805 | _ref3 = this.document.styleSheets; 806 | _results = []; 807 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 808 | styleSheet = _ref3[_l]; 809 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); 810 | } 811 | return _results; 812 | } 813 | }; 814 | 815 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { 816 | var e, rule, rules, styleNames, _i, _j, _len, _len1; 817 | try { 818 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 819 | } catch (_error) { 820 | e = _error; 821 | } 822 | if (!rules) { 823 | return; 824 | } 825 | for (_i = 0, _len = rules.length; _i < _len; _i++) { 826 | rule = rules[_i]; 827 | switch (rule.type) { 828 | case CSSRule.IMPORT_RULE: 829 | this.reloadStylesheetImages(rule.styleSheet, path, expando); 830 | break; 831 | case CSSRule.STYLE_RULE: 832 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 833 | styleNames = IMAGE_STYLES[_j].styleNames; 834 | this.reloadStyleImages(rule.style, styleNames, path, expando); 835 | } 836 | break; 837 | case CSSRule.MEDIA_RULE: 838 | this.reloadStylesheetImages(rule, path, expando); 839 | } 840 | } 841 | }; 842 | 843 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { 844 | var newValue, styleName, value, _i, _len; 845 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { 846 | styleName = styleNames[_i]; 847 | value = style[styleName]; 848 | if (typeof value === 'string') { 849 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { 850 | return function(match, src) { 851 | if (pathsMatch(path, pathFromUrl(src))) { 852 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; 853 | } else { 854 | return match; 855 | } 856 | }; 857 | })(this)); 858 | if (newValue !== value) { 859 | style[styleName] = newValue; 860 | } 861 | } 862 | } 863 | }; 864 | 865 | Reloader.prototype.reloadStylesheet = function(path) { 866 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; 867 | links = (function() { 868 | var _i, _len, _ref, _results; 869 | _ref = this.document.getElementsByTagName('link'); 870 | _results = []; 871 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 872 | link = _ref[_i]; 873 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { 874 | _results.push(link); 875 | } 876 | } 877 | return _results; 878 | }).call(this); 879 | imported = []; 880 | _ref = this.document.getElementsByTagName('style'); 881 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 882 | style = _ref[_i]; 883 | if (style.sheet) { 884 | this.collectImportedStylesheets(style, style.sheet, imported); 885 | } 886 | } 887 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { 888 | link = links[_j]; 889 | this.collectImportedStylesheets(link, link.sheet, imported); 890 | } 891 | if (this.window.StyleFix && this.document.querySelectorAll) { 892 | _ref1 = this.document.querySelectorAll('style[data-href]'); 893 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 894 | style = _ref1[_k]; 895 | links.push(style); 896 | } 897 | } 898 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); 899 | match = pickBestMatch(path, links.concat(imported), (function(_this) { 900 | return function(l) { 901 | return pathFromUrl(_this.linkHref(l)); 902 | }; 903 | })(this)); 904 | if (match) { 905 | if (match.object.rule) { 906 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); 907 | this.reattachImportedRule(match.object); 908 | } else { 909 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); 910 | this.reattachStylesheetLink(match.object); 911 | } 912 | } else { 913 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); 914 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { 915 | link = links[_l]; 916 | this.reattachStylesheetLink(link); 917 | } 918 | } 919 | return true; 920 | }; 921 | 922 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { 923 | var e, index, rule, rules, _i, _len; 924 | try { 925 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 926 | } catch (_error) { 927 | e = _error; 928 | } 929 | if (rules && rules.length) { 930 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { 931 | rule = rules[index]; 932 | switch (rule.type) { 933 | case CSSRule.CHARSET_RULE: 934 | continue; 935 | case CSSRule.IMPORT_RULE: 936 | result.push({ 937 | link: link, 938 | rule: rule, 939 | index: index, 940 | href: rule.href 941 | }); 942 | this.collectImportedStylesheets(link, rule.styleSheet, result); 943 | break; 944 | default: 945 | break; 946 | } 947 | } 948 | } 949 | }; 950 | 951 | Reloader.prototype.waitUntilCssLoads = function(clone, func) { 952 | var callbackExecuted, executeCallback, poll; 953 | callbackExecuted = false; 954 | executeCallback = (function(_this) { 955 | return function() { 956 | if (callbackExecuted) { 957 | return; 958 | } 959 | callbackExecuted = true; 960 | return func(); 961 | }; 962 | })(this); 963 | clone.onload = (function(_this) { 964 | return function() { 965 | _this.console.log("LiveReload: the new stylesheet has finished loading"); 966 | _this.knownToSupportCssOnLoad = true; 967 | return executeCallback(); 968 | }; 969 | })(this); 970 | if (!this.knownToSupportCssOnLoad) { 971 | (poll = (function(_this) { 972 | return function() { 973 | if (clone.sheet) { 974 | _this.console.log("LiveReload is polling until the new CSS finishes loading..."); 975 | return executeCallback(); 976 | } else { 977 | return _this.Timer.start(50, poll); 978 | } 979 | }; 980 | })(this))(); 981 | } 982 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); 983 | }; 984 | 985 | Reloader.prototype.linkHref = function(link) { 986 | return link.href || link.getAttribute('data-href'); 987 | }; 988 | 989 | Reloader.prototype.reattachStylesheetLink = function(link) { 990 | var clone, parent; 991 | if (link.__LiveReload_pendingRemoval) { 992 | return; 993 | } 994 | link.__LiveReload_pendingRemoval = true; 995 | if (link.tagName === 'STYLE') { 996 | clone = this.document.createElement('link'); 997 | clone.rel = 'stylesheet'; 998 | clone.media = link.media; 999 | clone.disabled = link.disabled; 1000 | } else { 1001 | clone = link.cloneNode(false); 1002 | } 1003 | clone.href = this.generateCacheBustUrl(this.linkHref(link)); 1004 | parent = link.parentNode; 1005 | if (parent.lastChild === link) { 1006 | parent.appendChild(clone); 1007 | } else { 1008 | parent.insertBefore(clone, link.nextSibling); 1009 | } 1010 | return this.waitUntilCssLoads(clone, (function(_this) { 1011 | return function() { 1012 | var additionalWaitingTime; 1013 | if (/AppleWebKit/.test(navigator.userAgent)) { 1014 | additionalWaitingTime = 5; 1015 | } else { 1016 | additionalWaitingTime = 200; 1017 | } 1018 | return _this.Timer.start(additionalWaitingTime, function() { 1019 | var _ref; 1020 | if (!link.parentNode) { 1021 | return; 1022 | } 1023 | link.parentNode.removeChild(link); 1024 | clone.onreadystatechange = null; 1025 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; 1026 | }); 1027 | }; 1028 | })(this)); 1029 | }; 1030 | 1031 | Reloader.prototype.reattachImportedRule = function(_arg) { 1032 | var href, index, link, media, newRule, parent, rule, tempLink; 1033 | rule = _arg.rule, index = _arg.index, link = _arg.link; 1034 | parent = rule.parentStyleSheet; 1035 | href = this.generateCacheBustUrl(rule.href); 1036 | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 1037 | newRule = "@import url(\"" + href + "\") " + media + ";"; 1038 | rule.__LiveReload_newHref = href; 1039 | tempLink = this.document.createElement("link"); 1040 | tempLink.rel = 'stylesheet'; 1041 | tempLink.href = href; 1042 | tempLink.__LiveReload_pendingRemoval = true; 1043 | if (link.parentNode) { 1044 | link.parentNode.insertBefore(tempLink, link); 1045 | } 1046 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { 1047 | return function() { 1048 | if (tempLink.parentNode) { 1049 | tempLink.parentNode.removeChild(tempLink); 1050 | } 1051 | if (rule.__LiveReload_newHref !== href) { 1052 | return; 1053 | } 1054 | parent.insertRule(newRule, index); 1055 | parent.deleteRule(index + 1); 1056 | rule = parent.cssRules[index]; 1057 | rule.__LiveReload_newHref = href; 1058 | return _this.Timer.start(_this.importCacheWaitPeriod, function() { 1059 | if (rule.__LiveReload_newHref !== href) { 1060 | return; 1061 | } 1062 | parent.insertRule(newRule, index); 1063 | return parent.deleteRule(index + 1); 1064 | }); 1065 | }; 1066 | })(this)); 1067 | }; 1068 | 1069 | Reloader.prototype.generateUniqueString = function() { 1070 | return 'livereload=' + Date.now(); 1071 | }; 1072 | 1073 | Reloader.prototype.generateCacheBustUrl = function(url, expando) { 1074 | var hash, oldParams, originalUrl, params, _ref; 1075 | if (expando == null) { 1076 | expando = this.generateUniqueString(); 1077 | } 1078 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; 1079 | if (this.options.overrideURL) { 1080 | if (url.indexOf(this.options.serverURL) < 0) { 1081 | originalUrl = url; 1082 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); 1083 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); 1084 | } 1085 | } 1086 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { 1087 | return "" + sep + expando; 1088 | }); 1089 | if (params === oldParams) { 1090 | if (oldParams.length === 0) { 1091 | params = "?" + expando; 1092 | } else { 1093 | params = "" + oldParams + "&" + expando; 1094 | } 1095 | } 1096 | return url + params + hash; 1097 | }; 1098 | 1099 | return Reloader; 1100 | 1101 | })(); 1102 | 1103 | }).call(this); 1104 | 1105 | },{}],8:[function(require,module,exports){ 1106 | (function() { 1107 | var CustomEvents, LiveReload, k; 1108 | 1109 | CustomEvents = require('./customevents'); 1110 | 1111 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); 1112 | 1113 | for (k in window) { 1114 | if (k.match(/^LiveReloadPlugin/)) { 1115 | LiveReload.addPlugin(window[k]); 1116 | } 1117 | } 1118 | 1119 | LiveReload.addPlugin(require('./less')); 1120 | 1121 | LiveReload.on('shutdown', function() { 1122 | return delete window.LiveReload; 1123 | }); 1124 | 1125 | LiveReload.on('connect', function() { 1126 | return CustomEvents.fire(document, 'LiveReloadConnect'); 1127 | }); 1128 | 1129 | LiveReload.on('disconnect', function() { 1130 | return CustomEvents.fire(document, 'LiveReloadDisconnect'); 1131 | }); 1132 | 1133 | CustomEvents.bind(document, 'LiveReloadShutDown', function() { 1134 | return LiveReload.shutDown(); 1135 | }); 1136 | 1137 | }).call(this); 1138 | 1139 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ 1140 | (function() { 1141 | var Timer; 1142 | 1143 | exports.Timer = Timer = (function() { 1144 | function Timer(func) { 1145 | this.func = func; 1146 | this.running = false; 1147 | this.id = null; 1148 | this._handler = (function(_this) { 1149 | return function() { 1150 | _this.running = false; 1151 | _this.id = null; 1152 | return _this.func(); 1153 | }; 1154 | })(this); 1155 | } 1156 | 1157 | Timer.prototype.start = function(timeout) { 1158 | if (this.running) { 1159 | clearTimeout(this.id); 1160 | } 1161 | this.id = setTimeout(this._handler, timeout); 1162 | return this.running = true; 1163 | }; 1164 | 1165 | Timer.prototype.stop = function() { 1166 | if (this.running) { 1167 | clearTimeout(this.id); 1168 | this.running = false; 1169 | return this.id = null; 1170 | } 1171 | }; 1172 | 1173 | return Timer; 1174 | 1175 | })(); 1176 | 1177 | Timer.start = function(timeout, func) { 1178 | return setTimeout(func, timeout); 1179 | }; 1180 | 1181 | }).call(this); 1182 | 1183 | },{}]},{},[8]); 1184 | -------------------------------------------------------------------------------- /src/wasp/version.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | NAME = "Wasp" 3 | DESC = "A Static Site Generator" 4 | VERSION = "0.1.1" 5 | end 6 | -------------------------------------------------------------------------------- /src/wasp/watcher.cr: -------------------------------------------------------------------------------- 1 | module Wasp 2 | class Watcher 3 | getter rules 4 | 5 | struct WatcherFile 6 | property path, mtime, status 7 | 8 | def initialize(@path : String, @mtime : Time) 9 | end 10 | end 11 | 12 | enum WatchFileStatus 13 | ADDED 14 | CHANGED 15 | DELETED 16 | end 17 | 18 | @source_path : String 19 | @rules : Array(String) 20 | @files : Hash(String, WatcherFile) 21 | 22 | def initialize(source_path : String, @rules = ["config.yml", "contents/**/*.md", "layouts/**/*.html", "static/**/*"]) 23 | @source_path = File.expand_path(source_path) 24 | @files = collect_files 25 | end 26 | 27 | def start 28 | loop do 29 | watch_changes do |file, status| 30 | puts "#{file} was #{status}" 31 | end 32 | sleep 1 33 | end 34 | end 35 | 36 | def watch_changes 37 | @files.each do |file, watch| 38 | if File.exists?(file) 39 | latest_file_mtime = file_mtime(file) 40 | if latest_file_mtime != watch.mtime 41 | yield file, WatchFileStatus::CHANGED 42 | 43 | watch.mtime = latest_file_mtime 44 | @files[file] = watch 45 | end 46 | else 47 | yield file, WatchFileStatus::DELETED 48 | end 49 | end 50 | 51 | files = collect_files 52 | if files.size > @files.size 53 | new_files = files.keys - @files.keys 54 | new_files.each do |file| 55 | yield file, WatchFileStatus::ADDED 56 | end 57 | end 58 | @files = files 59 | end 60 | 61 | def new_files 62 | new_files = collect_files 63 | if new_files.size > @files.size 64 | end 65 | end 66 | 67 | private def collect_files 68 | files = {} of String => WatcherFile 69 | @rules.each do |path| 70 | Dir.glob(File.join(@source_path, path)).each do |file| 71 | files[file] = WatcherFile.new(file, file_mtime(file)) 72 | end 73 | end 74 | 75 | files 76 | end 77 | 78 | private def file_mtime(file) 79 | File.info(file).modification_time 80 | end 81 | end 82 | end 83 | --------------------------------------------------------------------------------